블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2016. 10. 23. 22:59 Angular/Concept

Angular v2가 정식 릴리즈되었다. Angular v1 은 Two-way data-binding 이라는 독특한 특징으로 인해 많은 사용자 층을 확보했지만 장점 만큼이나 성능상의 단점도 존재했었다. 또한 처음엔 쉬운듯 하면서 좀 더 깊게 들어가볼려고 하면 학습곡선이 갑자기 껑충뛰기도 했다. 가장 많이 사용했던 Directive(지시자)가 대표적이다. 많은 개발자가 만들어 놓은 지시자를 쉽게 가져다 쓸 수는 있지만 직접 만들어 애플리케이션에 접목하려 할 때 첫 문턱을 만나게 된다. 그리고 jQuery사용에 익숙한 개발자에게 Angular v1 시점상의 차이로 Angular v1 방식의 개발패턴을 요구하기도 했다. 관성은 무섭다. 기존에 사용하던 방식을 버리고 Angular v1에 맞춰서 애플리케이션을 만들어 가기란 곤혹스럽다. Angular v2 또한 그런 인식의 전환을 요구할까? 그렇다 그리고 아니다. 






웹 애플리케이션 흐름

웹 애플리케이션 개발을 위해 우리가 사용하는 jQuery같은 라이브러리나 Angular, Backbone같은 프레임워크의 가장 1차적인 목적은 무엇일까? 나는 Data Projection이라 생각한다. 데이터를 화면에 출력하기 위해 DOM을 얼마나 쉽게 조작하고 상호 작용할 수 있느냐가 선택의 기준이라 생각한다. Data Projection을 일관되고 확장가능하고 배포가능하게 하는 방식으로 기술은 발전해 왔고, 현재는 화면에 대한 제어방식이 컴포넌트 기반 방식으로 발전해 오고 있다. 


Data Projection의 역사를 보면 초장기엔 Server Side Rendering 를 사용해 웹 애플리케이션을 개발했다. 예로 JSP, PHP, ASP 같이 서버 미들웨어서 데이터를 조회하고 HTML을 조작하여 결과 HTML을 브라우져에 전송하던 시대이다. 




1세대에는 AJAX가 나오고 다양한 라이브러리나 프레임워크가 나왔다. 이때는 데이터변경에 대한 DOM반영이 서버에서 클라이언트 개발자의 몫으로 넘어오게 되었다. 즉, 직접 DOM 을 얻어와서 특정 위치에 넣어 주어야 했고, DOM에서 발생하는 이벤트를 Listening해서 처리하고 DOM에 반영하는 모든 작업이 웹 개발자가 직접 코딩하던 단계였다. Java의 프레임워크 역사로 보면 Struts 로 비유할 수 있지 않을까 싶다. 




2세대로 넘어오게 되면 Model을 DOM 에 반영하는 방식은 자동화 된다. 여기에 대표적인 프레임워크가 Ember 와 Angular v1 이다. 이때 부터 Single Page Application (SPA) 개발이라는 용어가 나오게 된다. URI 변경에 대한 대응으로 Routing  개념이 나오고, Data Projection후 원하는 일부 DOM을 변경하는 역할이 프레임워크로 넘어갔고, 웹 개발자는 좀 더 애플리케이션 비즈니스 로직에 집중토록 만들었다. Java 프레임워크로 비유하자면 Struts와 Spring Framework 초기버전의 중간 지대 정도 쯤이라 생각한다.  이때부터 Frontend (프론트앤드)라는 직군이 웹 개발자와 분리되기 시작한 지점이라 생각한다. 이에 대한 자세한 설명은 태곤님이 작성한 "[번역] 프론트엔드 개발자는 왜 구하기 어렵나요?"를 참조하자. 2011년을 기점으로 2013년 웹 애플리케이션 프레임워크가 정착을 해가는 시기였고, 현재는 대부분의 스타트업이나 중견기업에서 2세대 웹 애플리케이션 프레임워크를 선택할 경우 프론트엔드 개발자와 백앤드 개발자를 구분하여 팀을 구성하고 있는 추세이다.





3세대는 2세대의 과도기를 거쳐 2세대의 장점을 흡수 하면서 성능상의 이슈를 해결하고, 점점 복잡해 지고있는 웹 애플리케이션을 보다 직관적이고 쉽게 개발할 수 있게 노력하고 있다. 대표적인 프레임워크로는 Facebook의 React와 Google의 Angular v2 (이하 Angular)이다. Angular는 Component기반 개발 방식으로 표준인 Web Components를 지원하며 Typescript를 기본 언어로 채택했다. Typescript는 Type 시스템을 제공하기 때문에 개발단계에서 버그의 가능성을 쉽게 찾을 수 있도록 도와준다. React와 Angular에 대한 장단점은 손창욱님의 "React보다 Angular v2에 더 주목해야 하는 이유"를 참조하자. Java의 Spring Framework이 성숙하면서 Annotation 같은 기능이 추가되듯, Angular v2 프레임워크는 Java의 Spring 프레임워크 최신버전과 비유할 수 있다. 



Angular v1에 대한 개발 및 컨설팅을 3년 가까이 하면서 올해 초 Angular v2를 공부하고 기존 v1 코드를 v2 코드로 전환하면서 코드 베이스는 50%가량 줄었고, 반응속도는 30%가량 개선되었다. 8명 프론트앤드 개발자와 컨버전을 진행하면서 이구동성으로 말하는 것은 "코드가 직관적으로 변했다. 코드량이 현저히 줄었다. Typescript의 타입체킹으로 인해 실수를 최소화 할 수 있었다" 이다. 



Angular v2 왜 배워야 하는가?

Angular를 왜 배워야 하는가? 답하자면 안배워도 된다. 단순 홈페이지나 업무 화면이라면 쉽고 더 빨리 만들 수 있는 워드프레스나 서비스를 이용하거나 DOM 핸들링 라이브러리나 플러그인을 사용해 개발하는 편이 낫다. 하지만 솔루션의 복잡한 요구사항을 지속적으로 반영해야 하고 DOM제어가 복잡해 질 가능성이 높다면 jQuery, React 같은 라이브러리 보다는 Angular 같은 프레임워크를 선택하는 것이 좋다. 그리고 최근에는 ES2015 표준이 확정되었고 최신 브라우져에 대부분 기능이 구현되고 있다. 2세대와 3세대 Data Projection의 가장 큰 개발 방식의 차이는 ES2015의 이해에서부터 시작한다.  즉, ES2015 문법을 잘 알고 사용하면 좀 더 쉽고 간단하게 코드 베이스를 유지하면서 오류를 최소화할 수 있다. 예로 -> 펑션은 this에 대한 오류를 방지하고, Set/Map등 Collection은 Java의 Collection과 유사한다. 



Angular v2 시작하면 초기에 배워야 하는 것들이 갑자기 늘어난다. 이것은 2세대와 3세대의 개발 패턴이 바뀌었음을 시사한다. ES2015 문법은 그대로 TypeScript에 녹아 있고, Type System과 Annotation 기능이 녹아 들어 더욱 편리한 개발을 가능토록 한다. 따라서 ES2015의 Syntax와 개념을 이해해야 한다. 그리고 Typescript를 다시 공부해야 한다. 또한 요즘 인기를 누리고 있는 Reactive Programming을 표방한 대표적인 라이브러리인 RxJS를 Angular가 근간으로 사용하고 있다. 따라서 RxJS 에 대한 개념과 사용법을 익혀야 한다. 그런후 Web Components 란 무엇인지 알아야 하고, Angular 프레임워크의 아키텍쳐를 구성하는 개념인 Change Detection 동작원리, Dependency Management, Modulization 을 알아야 하고, 다음으로 주변의 Tooling System으로 SystemJS (Webpack), Gulp 등을 알아야 한다. 


이렇게 열거해 보니 참으로 배울 것이 많다. 다시 말하지만 안 배워도 된다. 하지만 자신의 근육을 한단계 업그레이드 시키기 위해 고통스러운 인내의 시간은 필요하다. 배워야 하는 기준은 두가지 정도로 이야기 해본다. 


첫째, 서비스 버전업을 위해 요구사항이 계속 증가하고 있는가?

둘째, 더 적고 직관적인 코드 베이스를 유지하면서 성능을 높이고 싶은가?


 

프론트엔드 개발자 직군이 새롭게 자리잡게된  5년기간 동안 많은 부분이 기존의 백앤드 개발 패턴과 유사해 지고 있다. 모듈 의존성 관리, 빌드 시스템, 프레임워크의 발전은 Java개발자들이 초장기 프레임워크 없이 개발하다 Struts를 만났을 때 기쁨에서 Spring을 만나 자유를 얻었지만 여전히 배워야 할 것들은 더욱 증가했음을 알것이다. 그러나 어쩌겠는가 우리는 더 게을러 지고싶다는 욕구가 있고 프레임워크가 그것을 만족시켜줄것이라는 희망을 품고 있는 한 배움과 진보는 계속될 뿐이다. 



참조


posted by 윤영식
2013. 11. 20. 09:11 My Projects/BI Dashboard

이전 블로그에서 myBatis를 사용하면서 DAO Interface를 만들고, DAO를 구현한 클래스를 사용하는 방법을 테스트해 보았다. DAO를 구현하지 않고 Interface의 메소드만 선언하면 myBatis에서 자동 구현되어 사용할 수 있게 하는 방법을 알아보자 




1. Mapper 인식하는 방식

  - Spring context xml에 설정하기 : UserMapper는 단순 Interface

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />

  <property name="sqlSessionFactory" ref="sqlSessionFactory" />

</bean>

  - Mapper scanner를 등록하는 방식 : component-scan과 유사하게 mapper를 검색해 준다 (안됨)

<beans xmlns="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"

  xsi:schemaLocation="

  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

  http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

 

  <mybatis:scan base-package="org.mybatis.spring.sample.mapper" />


</beans>

  - @MapperScan 애노테이션 사용 : java로 환경설정

@Configuration

@MapperScan("org.mybatis.spring.sample.mapper")

public class AppConfig {


  @Bean

  public DataSource dataSource() {

    return new EmbeddedDatabaseBuilder().addScript("schema.sql").build()

  }


  @Bean

  public SqlSessionFactory sqlSessionFactory() throws Exception {

    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();

    sessionFactory.setDataSource(dataSource());

    return sessionFactory.getObject();

  }

}

  - MapperScannerConfigurer 사용 : 본 셋팅에는 해당 설정을 사용한다 

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

  <property name="basePackage" value="com.mobiconsoft.dashboard.mapper" />

</bean>



2. pom.xml의 myBatis 버전 업그레이드 

  - mybatis-spring.jar 에서 1.2 이상의 최신 버전을 사용토록 한다 

<mybatis.spring.version>1.2.1</mybatis.spring.version>

<mysql.connector.version>5.1.27</mysql.connector.version>


<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>${mybatis.version}</version>

</dependency>

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis-spring</artifactId>

<version>${mybatis.spring.version}</version>

</dependency>



3. myBatis Mapper 환경 설정

  - 위치를 webapp 폴더 밑으로 옮긴다 

    

  - dbpool-context.xml의 환경설정 변경 : id 명칭은 내부적으로 동일 이름을 사용하니 변경하지 말자 

<!-- mybatis sql session template -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

        <property name="dataSource" ref="dataSource" />

<property name="mapperLocations" value="classpath:mappers/*Mapper.xml" />

<property name="configLocation" value="/WEB-INF/mybatis-config.xml" />

</bean>


<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">

<constructor-arg ref="sqlSessionFactory" />

</bean>


<!-- mybatis mapper auto scanning -->

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="com.mobiconsoft.dashboard.mapper" />

</bean>

  - mybatis-config.xml 내역 수정

<configuration>

    <settings>

        <setting name="cacheEnabled" value="false" />

        <setting name="useGeneratedKeys" value="true" />

        <setting name="defaultExecutorType" value="REUSE" />

    </settings>

</configuration>

  - Person.xml 맵퍼 xml 파일명칭을 PersonMapper.xml 로 변경하고 WEB-INF/classes/mappers/ 폴더 밑으로 이동한다 

    + namespace는 반드시 PersonMapper interface 파일이 있는 위치를 명시한다 

    + resultType, parameterType 시에 full package + class 명칭을 명시한다 


<mapper namespace="com.mobiconsoft.dashboard.mapper.PersonMapper">

 

    <resultMap id="person" type="com.mobiconsoft.dashboard.domain.Person" >

        <result property="id" column="id"/>

        <result property="name" column="name"/>

    </resultMap>

 

    <select id="getPersons" resultType="com.mobiconsoft.dashboard.domain.Person">

        SELECT

        *

        FROM

        person

    </select>

 

    <select id="getPerson" parameterType="Integer" resultType="com.mobiconsoft.dashboard.domain.Person">

        SELECT

        *

        FROM

        person

        WHERE

        id=#{id}

    </select>

 

    <insert id="savePerson" parameterType="com.mobiconsoft.dashboard.domain.Person"

        INSERT INTO

        person(id, name)

        VALUES

        (#{id}, #{name})

    </insert>

    

    <update id="updatePerson" parameterType="com.mobiconsoft.dashboard.domain.Person"> 

        UPDATE 

        person 

        SET

        name=#{name}

        WHERE

        id=#{id} 

    </update>

 

    <delete id="deletePerson" parameterType="Integer">

        DELETE FROM

        person

        WHERE

        id=#{id}

    </delete>

 

</mapper>

  - 이제 PersonDAO.java interface를 PersonMapper.java로 바꾸어 보자 

    + PersonMapper.xml 에서 정의한 parameterType은 메소드의 파라미터 타입와 일치해야 한다

    + PersonMapper.xml 에서 정의한 resultType은 메소드의 리턴 타입과 일치해야 한다 

@Repository(value="personMapper")

public interface PersonMapper {

public List<Person> getPersons();

public Person getPerson(int id);

public void savePerson(Person person);

public void updatePerson(Person person);

public void deletePerson(int id);

}

  - grunt build 할 때 WEB-INF/ 밑의 모든 폴더/파일 dist 폴더에 copy하는 옵션 변경

copy: {

      dist: {

        files: [{

          expand: true,

          dot: true,

          cwd: '<%= yeoman.app %>',

          dest: '<%= yeoman.dist %>',

          src: [

            '*.{ico,png,txt,html}',

            '.htaccess',

            'bower_components/**/*',

            'images/{,*/}*.{gif,webp}',

            'styles/fonts/*',

            'WEB-INF/**/*'

          ]


  - PersonService Interface 제거하고 구현체를 PersonService로 변경하였음. 최종 소스 디렉토리 

    + controller : RESTful 요청 처리 

    + service : 실제 업무 트랜잭션 @Transactional 처리

    + mapper : myBatis mapper interface 

    + domain : 비즈니스 객체. 주로 테이블과 1:1 맵핑되는 Model

    

  - 실행

    

* 저장소 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_mybatis_mapper



<참조>

  - myBatis Mapper 설정방법

posted by 윤영식
2013. 11. 20. 01:33 My Projects/BI Dashboard

데이터 저장소를 MySQL을 사용하며 CRUD를 위하여 MyBatis를 사용한다. Spring 프레임워크에서 사용하기 위한 설정과 사용법을 알아보자 



1. JDBC Pool 설정

  - tomcat 7을 사용하므로 tomcat의 jdbc-pool을 사용한다 (참조)

  - maven pom.xml : 이미 $TOMCAT_HOME/lib 밑에 라이브러리가 존재할 경우 provided 설정

<!-- spring jdbc dependency spring-tx -->

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-jdbc</artifactId>

<version>${spring.version}</version>

</dependency>


<!-- tomcat jdbc -->

<dependency>

<groupId>org.apache.tomcat</groupId>

<artifactId>tomcat-jdbc</artifactId>

<version>${tomcat.jdbc.version}</version>

<!--scope>provided</scope-->

</dependency>

  - spring context 와 properties 설정하기 

// web.xml 에서 dbpool-context.xml을 인식하기 위하여 설정변경

    <!-- root-context.xml, mybatis-context.xml -->

    <context-param>

        <param-name>contextConfigLocation</param-name>

        <param-value>

            /WEB-INF/*-context.xml

        </param-value>

    </context-param>


// dbpool-context.xml 에서 properties 에서 환경값을 읽어서 설정한다 

// common-jdbc, c3po보다 tomcat의 jdbc-pool 성능이 더 좋다 

<bean

class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="locations">

<value>/WEB-INF/dbpool.properties</value>

</property>

</bean>


<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource"

destroy-method="close">

<property name="driverClassName" value="${jdbc.driverClass}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

<property name="initialSize" value="${jdbc.min.size}" />

<property name="maxActive" value="${jdbc.max.size}" />

<property name="maxIdle" value="5" />

<property name="minIdle" value="2" />

</bean>


// dbpool.properties 내역 

jdbc.driverClass=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/SolarDB?autoReconnect=true

jdbc.username=admin

jdbc.password=admin

jdbc.min.size=2

jdbc.max.size=10



2. Transaction 설정

  - @Transactional 애노테이션 기반으로 트랜잭션을 관리하고자 할 경우 설정

  - DAO 보다는 가급적 DAO를 호출하는 Service Interface의 Insert, Update, Delete 메소드 선언할 때 @Transactional 설정 (참조)

// dbpool-context.xml 에서 dataSource를 TransactionManager에 할당한다 

<!-- transaction manager -->

<context:annotation-config/>

<tx:annotation-driven transaction-manager="transactionManager" />

<bean id="transactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"></property>

</bean>

  - @Transactional 사용시 Propagation 동작 방식에 대해서 숙지하자 (참조)

  - Spring과 그외 관련 설정 파일을 WEB-INF/spring 폴더로 위치 변경함

    



3. myBatis 설정

  - DataSource와 Transaction에 대한 설정이 끝났다면 myBatis를 설정한다 

  - MySQL을 사용할 것이다

  - myBatis의 Resource인 config와 mapper xml 파일은 src/main/resource로 이동하자 (Person.xml, mybatis-config.xml)

    


// dbpool-context.xml 에서 dataSource를 설정한다

// SqlSessionTemplate 을 DAO에서 사용할 것이다 

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

<property name="configLocation" value="classpath:mybatis-config.xml" />

</bean>

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">

<constructor-arg ref="sqlSessionFactory" />

</bean>



// mybatis-config.xml 에서 mapper.xml의 위치를 설정 

// Person.xml 과 같은 mapper xml을 여러개 열거 할 수 있다 

<configuration>

    <settings>

        <setting name="cacheEnabled" value="false" />

<setting name="useGeneratedKeys" value="true" />

<setting name="defaultExecutorType" value="REUSE" />

    </settings>

    

    <typeAliases>

        <typeAlias alias="Person" type="com.mobiconsoft.dashboard.domain.Person"></typeAlias>

    </typeAliases>

    

    <mappers>

        <mapper resource="com/mobiconsoft/dashboard/mybatis/mapper/Person.xml"></mapper>

    </mappers>

</configuration>

  - Mysql을 설치 및 Person Table을 하나 만들고 Mapper xml을 설정한다. 여기서는 예제로 Person.xml 파일 하나만 설정함 

// mysql 에서 테이블 생성하기 (참조)

mysql> create table person(

    -> id INT,

    -> name VARCHAR(30));


// Person.xml 맵핑 xml 내역 

// 호출 방법 ID : <namespace>.<id> ex) com.mobiconsoft.dashboard.person

<mapper namespace="com.mobiconsoft.dashboard.Person">

 

    <resultMap type="Person" id="PersonResult">

        <id property="id" column="id"/>

        <result property="name" column="name"/>

    </resultMap>

 

    <select id="getPersons" resultMap="PersonResult">

        SELECT

        *

        FROM

        person

    </select>

 

    <select id="getPerson" parameterType="Integer" resultMap="PersonResult">

        SELECT

        *

        FROM

        person

        WHERE

        id=#{id}

    </select>

 

    <insert id="savePerson" parameterType="Person"> <!-- useGeneratedKeys="true" keyProperty="id"> -->

        INSERT INTO

        person(id, name)

        VALUES

        (#{id}, #{name})

    </insert>

    

    <update id="updatePerson" parameterType="Person"

        UPDATE 

        person 

        SET

        name=#{name}

        WHERE

        id=#{id} 

    </update>

 

    <delete id="deletePerson" parameterType="Integer">

        DELETE FROM

        person

        WHERE

        id=#{id}

    </delete>

</mapper>

  - DAO와 DAO를 구현한다. 

// DAO Interface 

public interface PersonDAO {

  public List<Person> getPersons();

  public Person getPerson(int id);

  public Person savePerson(Person person);

  public Person updatePerson(Person person);

  public void deletePerson(int id);

}


// DAO Implementation

@Repository

public class PersonDAOImpl implements PersonDAO {

@Autowired

private SqlSession sqlSession;

private static final Logger logger = LoggerFactory.getLogger(PersonDAOImpl.class);

// end must be point . 

private static final String NS = "com.mobiconsoft.dashboard.Person.";


@Override

public List<Person> getPersons() {

logger.info("dao select all");

return sqlSession.selectList(NS+"getPersons");

}


@Override

public Person getPerson(int id) {

logger.info("dao select one id is " + id);

return sqlSession.selectOne(NS+"getPerson");

}


@Override

public Person savePerson(Person person) {

logger.info("dao save person is " + person);

sqlSession.insert(NS+"savePerson", person);

return person;

}

@Override

public Person updatePerson(Person person) {

logger.info("dao update person is " + person);

sqlSession.update(NS+"updatePerson", person);

return person;

}


@Override

public void deletePerson(int id) {

logger.info("dao delete id is " + id);

sqlSession.delete(NS+"deletePerson", id);

}

}

  - 기존 PersonServiceImpl의 내역을 변경한다 : @Transactional을 통하여 트랜잭션을 Service 레벨에서 관리한다 

@Service

public class PersonServiceImpl implements PersonService {


@Autowired

PersonDAO personDAO;

@Override

public List<Person> getPersons() {

return personDAO.getPersons();

}


@Override

public Person getById(Integer id) {

return personDAO.getPerson(id);

}


@Override

@Transactional

public Person save(Person person) {

personDAO.savePerson(person);

return person;

}

@Override

@Transactional

public Person update(Person person) {

personDAO.updatePerson(person);

return person;

}

@Override

@Transactional

public void delete(Integer id) {

personDAO.deletePerson(id);

}

}


* 저장소 : https://github.com/ysyun/SPA_Angular_SpringFramework_Eclipse_ENV/tree/feature_mybatis



<참조>

  - Spring, myBatis 설정 방법

  - Tomcat 7일 경우 Spring에서 Tomcat JDBC-Pool 사용하기

  - Common-JDBC와 C3P0와 Tomcat JDBC-Pool 성능차이

  - Transaction Manager 선언적으로 사용하기

  - Transaction Propagation 동작 방식

  - Mac에서 기존 MySQL 삭제하기 

posted by 윤영식
prev 1 next