Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7주차](이세원, 김동하, 박준영) #8

Open
sunwootest opened this issue Aug 8, 2023 · 2 comments
Open

[7주차](이세원, 김동하, 박준영) #8

sunwootest opened this issue Aug 8, 2023 · 2 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

sunwootest commented Aug 8, 2023

  • 7주차
    • 6.5 ~ 6.6장
    • 6.7 ~ 6.9장
    • 7.1 ~ 7.4장
@sunwootest sunwootest changed the title [6주차](이세원, 김동하, 박준영) [7주차](이세원, 김동하, 박준영) Aug 8, 2023
@leesewon00
Copy link
Member

📚6.7 ~ 6.9장 정리

트랜잭션 애노테이션

  • 포인트컷과 트랜잭션 속성을 이용해 트랜잭션을 일괄적으로 적용하는 방식은 대부분 상황에서 잘 들어맞음
  • 하지만, 세밀하게 튜닝된 트랜잭션 속성의 경우 포인트컷과 트랜잭션 속성 사용으로는 적합하지 않음
  • 포인트컷과 트랜잭션 속성과 같이 설정 파일에서 분류 가능한 그룹으로 만들어 일괄적으로 속성을 부여하는 대신,
    직접 타겟에 속성정보를 가진 애노테이션을 지정해 세밀하게 트랜잭션 속성을 적용함
  • 이 애노테이션을 트랜잭션 애노테이션이라 함

@transactional

  • 메서드와 클래스, 인터페이스에 적용될 수 있음
  • 스프링은 @transactional이 부여된 모든 객체를 자동으로 타겟 오브젝트로 인식
  • 이때 포인트컷은 TransactionAttributeSourcePointcut이 사용되고 애노테이션이 부여된 모든 객체를 찾아서 포인트컷의 선정 결과로 돌려줌
  • 트랜잭션 속성은 모든 항목을 엘리먼트로 지정할 수 있고 모두 디폴트 값을 가짐으로 생략할 수 있음
  • ex)
@Transactional(readOnly=true)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";
    @AliasFor("value")
    String transactionManager() default "";
    String[] label() default {};
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    String timeoutString() default "";
    boolean readOnly() default false;
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}

트랜잭션 속성을 이용하는 포인트컷

  • @transactional 애노테이션을 사용하면 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정할 수 있음
  • @transactional은 메서드마다 다르게 설정할 수도 있음으로 매우 유연한 트랜잭션 속성 설정이 가능
  • 그러나 메서드 마다 모두 속성을 지정하면 유연한 제어는 가능하겠지만 코드는 지저분해지고
    동일한 속성 정보를 가진 애노테이션을 반복적으로 메서드마다 부여해주는 바람직하지 못한 결과를 가져올 수 있음

대체 정책

  • 스프링은 트랜잭션 기능이 부여될 위치인 타겟 객체의 메서드부터 시작해서 클래스, 인터페이스 순으로
    @transactional 애노테이션이 존재하는지 확인하고 우선 적용
  1. 타깃 메소드 [5],[6]
  2. 타깃 클래스 [4]
  3. 선언 메소드 [2],[3]
  4. 선언 타입 [1]
[1]
public interface Service { 
	[2]
	void method1(); 
	[3]
	void method2();
}

[4]
public class Servicelmpl implements Service {
	[5]
	public void method1() (
	[6]
	public void method2() {
}
  • 메서드가 여러개라면 클래스 레벨에 공통 속성을 부여하고 공통 속성을 따르지 않는 메서드가 있다면 추가로 @transactional을 부여해 줄 수 있음
  • 기본적으로 @transactional 적용 대상은 클라이언트가 사용하는 인터페이스가 정의한 메서드이므로 @transactional도 타겟 클래스보다는 인터페이스에 두는 것이 바람직
  • 타겟에만 트랜잭션을 적용하겠다는 확신이 있다면 타깃 클래스에 직접 애노테이션을 부여할 수도 있음
  • 그러나 인터페이스에 @transactional을 두면 구현 클래스가 바뀌더라도 트랜잭션 속성을 유지할 수 있다는 장점이 있음

트랜잭션 애노테이션 적용

  • ex)
@Transactional
public interface UserService {
    void upgradeLevels();
    void add(User user);
    @Transactional(readOnly = true) // default : false
    User get(String id);
    @Transactional(readOnly = true)
    List<User> getAll();
    void deleteAll();
    void update(User user);
}

장점

  • 클래스, 메서드 이름에 일관된 패턴을 만들어 적용하고 이를 활용해 포인트컷과 트랜잭션 속성을
    지정하는 것보다는 단순하게 트랜잭션이 필요한 타입 또는 메서드에 직접 애노테이션을 부여하는 것이 훨씬 편리하고 코드를 이해하기도 좋음

단점과 주의사항

  • 트랜잭션 적용 대상을 손쉽게 파악할 수 없고, 트랜잭션이 적용되지 않았다는 사실을 파악하기가 쉽지 않음
  • 일반적으로는 트랜잭션이 적용되지 않았다고 기능이 동작하지 않는 것도 아니므로
    예외적인 상황이 발생해서 롤백이 필요한 시점이 되서야 트랜잭션 적용 여부를 확인해보게 됨
  • 그래서 주의가 요구되고 특정 프로젝트 시점에는
    트랜잭션이 정상적으로 적용되었는지 확인하는 과정이 반드시 요구됨

@transactional 테스트

테스트를 위한 트랜잭션 애노테이션

@transactional

  • @transactional을 사용하면 트랜잭션 경계설정이 자동 설정됨
  • 디폴트는 REQUIRED임 (Propagation propagation() default Propagation.REQUIRED;)

@Rollback

  • @transactional을 사용하는 애플리케이션 클래스와 테스트 클래스의 디폴트 속성은 동일함
  • 단, 테스트 클래스는 테스트 수행 후 자동으로 롤백됨
  • 테스트 수행 후 롤백을 원하지 않을 때 @Rollback 사용
  • @Rollback의 기본값은 true이기 때문에 false로 선언해야 롤백되지 않음
public class UserServiceTest {
    ...
    @Test
    @Transactional
    @Rollback(false)
    public void transactionSync() {
        ...
    }
}

@TransactionConfiguration

  • @Rollback은 메소드 레벨에서만 사용 가능함
  • 클래스 레벨에서 적용하기 위해 @TransactionConfiguration을 사용해야 함
  • @Rollback과 동일하게 false로 선언해야 롤백되지 않음
  • @TransactionConfiguration을 사용한 클래스에서, 특정 메소드는 롤백하고 싶을 경우 해당 메소드에만 @Rollback 사용하면 됨
  • 클래스보다 메소드가 우선순위가 더 높기 때문임
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
@Transactional
@TransactionConfiguration(defaultRollback = false) //롤백되지 않음
public class UserServiceTest {
    @Test
    @Rollback //롤백됨
    public void add(){}
}

@NotTransactional과 Propagation.NEVER

  • @transactional이 있는 클래스에서 특정 메소드를 트랜잭션 동작하고 싶지 않을 때 사용
  • @NotTransactional는 deprecated 되었음
  • 대신, Propagation.NEVER 사용
@Transactional(propagation = Propagation.NEVER)

정리

  • 테스트 내에서 트랜잭션을 제어할 수 있는 애노테이션을 잘 활용하면 통합 테스트를 만들 때 편리함
  • 단위 테스트와 통합 테스트는 클래스로 구분하는게 좋음
  • DB가 사용되는 통합 테스트는 롤백 테스트로 만드는 것이 좋음

@farmJun
Copy link

farmJun commented Aug 13, 2023

📚토비의 스프링 7장 스프링 핵심 기술의 응용

1️⃣ SQL과 DAO의 분리

이전 챕터들에서 반복적인 JDBC 작업 흐름은 템플릿을 이용해 DAO에서 완벽하게 제거했다.
하지만 아직도 DB 테이블과 필드 정보를 고스란히 담고 있는 SQL 문장은 분라하지 못했다.

DAO 인터페이스, JDBC라는 처리 기술, 정보를 담는 오브젝트가 바뀌지 않는다면 DAO 코드가 수정될 일은 없다.
하지만, 데이터 엑세스 로직이 바뀌지 않더라도 DB의 테이블, 필드 이름과 SQL 문장은 변경될 수 있다.
그래서 SQL의 변경이 필요하다면 어떻게 하더라도 DAO의 코드가 수정될 수 밖에 없다.
물론 개발 중이거나 운영 중인 시스템의 SQL을 변경해달라는 요청이 빈번히 일어나지는 않는다.
그렇다고 요청이 발생했을 때마다 DAO 코드를 수정하고 다시 컴파일하는 것은 번거로울 뿐만 아니라 위험하다.

따라서 SQL을 적절히 분리해 DAO 코드와 다른 파일이나 위치에 두고 관리할 수 있다면 좋을 것이다.

2️⃣ XML 설정을 이용한 분리

SQL을 스프링의 XML 설정 파일로 분리하는 것이다. 스프링은 설정을 이용해 값을 주입할 수 있다. SQL은 문자열로
되어 있으니 설정 파일에 프로퍼티 값으로 정의하여 DAO에 주입할 수 있다. 즉, 설정 파일에 있는 SQL을 독립적으로 수정 할 수 있다.

  1. 개별 SQL 프로퍼티 방식
public class UserDaoJdbc implements UserDao{
    private String sqlAdd;
    
    ...
}
-----
public void add(User user){
    this.jdbcTemplate.update(
        this.sqlAdd,
        user.getId(), user.getPassword(), ...
        ); 
    
}
-----
<bean id="userDao" class="~~~~~~">
    <property name="sqlAdd" value="insert into users(id, name, password)
        values (?,?,?)/>
</bean>

sqlAdd를 사용하는 SQL은 코드의 수정 없이 XML 설정을 바꿔서 자유롭게 수정이 가능하다.

  1. SQL 맵 프로퍼티 방식
public class UserDaoJdbc implements UserDao{
    private Map<String, String> sqlMap;
    
    ...
}
-----
public void add(User user){
    this.jdbcTemplate.update(
        this.sqlMap.get("add"),
        user.getId(), user.getPassword(), ...
        );
}
-----
<bean id="userDao" class="~~~~~~">
    <property name="sqlMap>
        <map>
            <entity key="add" value="insert into users(id, name, password)
             values (?,?,?) />
            <entity key="get" value="select * from users where id =?"/>
        </map>
</bean>

이렇게 맵으로 만들어두면 새로운 SQL이 필요할 때 <entry>만 추가해주면 된다. 대신 메소드에서 SQL을 가져올 때 문자열로 된 키 값을 사용하기 때문에 오타와 같은 실수가 있어도, 해당 메소드가 실행되기 전에는 오류를 확인하기 힘들다는 단점이 있다.

3️⃣ SQL 제공 서비스

설정파일 안에 SQL을 두고 이를 DI해서 DAO가 사용하게 하면 SQL을 분리할 수 있지만 몇 가지 문제점이 존재한다.

  1. SQL과 DI 설정 정보가 섞여 있으면 보기에도 지저분하고 관리하기에도 좋지 않다.
  2. 데이터 액세스 로직의 일부인 SQL 문장을 애플리케이션의 구성 정보를 가진 설정 정보와 함께 두는 것은 바람직하지 않다.
  3. 설정 파일로부터 생성된 오브젝트와 정보는 애플리케이션을 다시 시작하기 전에는 변경이 어렵다.

이러한 문제점을 해결하려면 DAO가 사용할 SQL을 제공해주는 기능을 독립시킬 필요가 있다. 즉, 독립적인 SQL 제공 서비스가 필요하다.

public interface SqlService{
    Strinig getSql(String key) throws someException;
}
------
public class SimpleSqlService implements SqlService{
    private Map<String, String> sqlMap;
    
    public String getSql(String key) throws someException{
        String sql = sqlMap.get(key);
        ...
    }
}

SQL 서비스는 SQL에 대한 키 값을 전달하면 그에 해당하는 SQL을 전달하는 기능을 한다.
키를 통해 어떻게 SQL을 가져오는지, SQL은 어디에 저장되어 있는지는 DAO의 관심사항이 아니다.
DAO는 적절한 키를 제공해주고 그에 대한 SQL을 받기만 하면 된다.

public class UserDaoJdbc implements UserDao{
    private SqlService sqlService;
    ...
}
-----
public void add(User user){
    this.jdbcTemplate.update(
        this.sqlService.getSql("userAdd"),
        user.getId(), user.getPassword(), ...
        );
}
-----
<bean id="userDao" class="~~~~~~">
    <property name="sqlService" ref="sqlService"/>
</bean>

<bean id="sqlService" class="~~~~~">
    <property name="sqlMap">
        <map>
            <entity key="userAdd" value="insert into users(id, name, password)
                    values (?,?,?) />
            <entity key="userGet" value="select * from users where id =?"/>
        </map>
    </property>
</bean>

코드와 설정만 놓고 보면 앞에서 사용했던 방법과 별로 다른 것이 없어 보인다.
sqlService 빈은 여전히 설정 파일 안에 <map>을 이용해 SQL 정보를 가져온다.
하지만 큰 차이가 존재한다. userDao를 포함한 모든 DAO는 SQL을 어디에 저장하고 가져오는지에 대해서는 신경쓰지 ㅇ낳아도 된다.
구체적인 구현 방법과 기술에 상관없이 sqlService 인터페이스 타입의 빈을 DI 받아서 필요한 SQL을 사용하면 된다.

4️⃣ 인터페이스 분리와 자기참조 빈

XML 파일 매핑

스프링 XML 설정 파일에서 <bean>태그 안에 SQL 정보를 넣어놓고 활용하는 것은 좋지 않다.
SQL을 저장해두는 전용 포맷을 가진 독립적인 파일을 이용하는 편이 바람직하다. 독립적이라고 해도 역시 XML이 가장 편리한 포맷이다.

JAXB(Java Architecture for XML Binding)

JAXB는 XML에 담긴 정보를 파일에서 읽는 방법 중 하나이다. XML 정보를 오브젝트처럼 다룰 수 있어 편리하다는 장점이 있다.

  1. 마샬링 : 자바 오브젝트를 XML로 매핑
  2. 언마샬링 : XML을 자바 오브젝트로 매핑

XML SQL 서비스

public class XmlSqlService implements SqlService {
    private Map<String, String> sqlMap = new HashMap<String, String>(); // 읽어온 SQL을 저장해둘 맵

    public XmlSqlService() {
        String contextPath = Sqlmap.class.getPackage().getName();
        try {
            JAXBContext context = JAXBContext.newInstance(contextPath);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            InputStream is = UserDao.class.getResourceAsStream("sqlmap.xml");
            Sqlmap sqlmap = (Sqlmap) unmarshaller.unmarshal(is);
            for (SqlType sql : sqlmap.getSql()) {
                sqlMap.put(sql.getKey(), sql.getValue());
            }
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

위 코드의 문제점

  1. 생성자에서 예외가 발생할 수도 있는 복잡한 초기화 작업을 다룬다.
  2. 읽어들일 파일의 위치와 이름이 코드에 고정되어 있다.

코드의 로직과 다른 이유로 바뀔 가능성이 있는 내용은 외부에서 DI로 설정해줄 수 있게 하자.

여기서 @PostConstruct를 사용할 것이다. @PostConstructjava.lang.annotation에 포함된 공통 어노테이션의 한가지로 JavaEE 5나 JDK6에 포함된 표준 어노테이션이다.
@PostConstruct를 초기화 작업을 수행할 메서드에 부여하면 스프링은 등록된 빈의 오브젝트를 생성하고 DI 작업을 마친 후에 @PostConstruct가 붙은 메서드를 자동으로 실행한다.
생성자와는 달리 프로퍼티까지 모두 준비된 후에 실행된다는 면에서 @PostConstruct초기화 메서드는 유용하다.

이렇게해도 SQL을 가져오는 방법이 특정 기술에 고정되어 있다. 다른 포맷의 파일을 읽어와야하면 XmlSqlService를 직접 수정해야한다.

여기서 분리 가능한 관심사가 생긴다.

  1. SQL 정보를 외부 리소스로부터 읽어오는 책임
  2. 읽어온 SQL을 보관해두고 있다가 필요할 때 제공하는 책임
  3. 한 번 가져온 SQL을 필요에 따라 수정
스크린샷 2023-08-14 오전 1 05 16

SqlReader는 읽어온 다음에 SqlRegistry에 전달해서 등록되게 해야한다.
SqlService가 SqlReader에게 데이터를 요청하고, 그것을 다시 SqlRegistry에 전달하는 방식은 불필요하게 service를 거치게 된다.
따라서, SqlReader에게 SqlRegistry 오브젝트를 전달해서 저장하도록 요청하는게 좋다. 즉, SqlRegistry가 일종의 콜백 오브젝트처럼 이용된다.
스크린샷 2023-08-14 오전 1 05 31

자기참조 빈으로 구현하자

책임에 따라 분리되지 않았던 XmlSqlService 클래스를 세분화된 책임을 정의한 인터페이스(SqlReader, SqlSeivice, SqlRegistry)를 구현하자.

SqlService 메서드에서

  1. SQL을 읽을 때는 SqlReader
  2. Sql을 찾을 때는 SqlRegistry

를 통해 간접적으로 접근하게 했다. 이제 빈 설정을 통해 실제 DI가 일어나도록 해야 한다.

<bean id="sqlService" class="~~~~~">
    <property name="sqlReader" ref="sqlService" />
    <property name="sqlRegistry" ref="sqlService" />
</bean>

자기 참조빈은 사실 흔히 쓰이는 방법이 아니다. 확장이 힘들고 변경에 취약한 구조의 클래스를 유연한 구조로 만들려고 할 때 처음 시도해볼 수 있는 방법이다.

5️⃣ 서비스 추상화 적용

OXM 서비스 추상화

XML과 자바 오브젝트를 매핑해서 상호 변환해주는 기술을 간단히 OXM(Object-XML Mapping)이라고도 한다.

JAXB 외에 로우 레벨의 구체적 기술과 API에 종속되지 않고 추상화된 레이어와 API를 제공해서 구현 기술에 대해 독립적인 코드를 작성할 수 있다.
SqlReader는 XML을 자바오브젝트로 변환하는 언마샬러 인터페이스가 필요하다.

public interface Unmarshaller {
    Object unmarshal(Source source) throws IOException, XmlMappingException;
}

이제 추상화된 OXM 기능을 이용하는 SqlService를 구현하자.

public class OxmSqlService implements SqlService {
    private final OxmSqlReader oxmSqlReader = new OxmSqlReader();
    
    private class OxmSqlReader implements SqlReader {
        ...
    }
}

SqlRegistry는 DI받고 SqlReader는 사용성 극대화를 위해 스프링의 OXM 언마샬러를 이용하도록 고정한다.

6️⃣ 인터페이스 상속을 통한 안전한 기능 확장

원칙적으로 권장되진 않지만 때로는 서버가 운영 중인 상태에서 서버를 재시작하지 않고 긴급하게 애플리케이션이 사용 중인 SQL을 변경해야 할 수도 있다.

DI와 기능의 확장

인터페이스를 사용해야 하는 이유

  1. 다형성을 얻기 위해서
  2. 인터페이스 분리 원칙 (ISP)

인터페이스를 사용한 DI의 관한 내용은 앞에서 다룬 내용이기에 따로 정리하지 않았습니다.

인터페이스와 DI를 통한 유연한 확장구조.jpg
스크린샷 2023-08-14 오전 1 06 01

인터페이스 상속을 이용한 확장구조.jpg
스크린샷 2023-08-14 오전 1 06 19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants