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

[6주차](이승철, 이세원, 권용현) #5

Open
sunwootest opened this issue Aug 1, 2023 · 3 comments
Open

[6주차](이승철, 이세원, 권용현) #5

sunwootest opened this issue Aug 1, 2023 · 3 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

sunwootest commented Aug 1, 2023

  • 6주차
    • 4장
    • 5장
    • 6.1 ~ 6.4장
@leesewon00
Copy link
Member

leesewon00 commented Aug 7, 2023

5장 서비스 추상화

환경과 상황에 따라서 기술이 바뀌고, 그에 따라 다른 API를 사용하고, 다른 스타일의 접근 방법을 따라야하는 건 피곤하다.
스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지를 살펴보자.

예시 1) 트랜잭션 서비스 추상화

기존 상황

기존상황

JdbcTemplate의 메소드를 사용하는 UserDao는 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행된다.
UserDao는 update() 메소드가 호출될 때마다 JdbcTemplate을 통해 매번 새로운 DB 커넥션과 트랜잭션을 만들어 사용한다.
어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용돼야 한다.

image

UserService와 UserDao를 그대로 둔 채로 트랜잭션을 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
트랜잭션 경계를 upgradeLevels() 메소드 안에 두려면 DB 커넥션도 이 메소드 안에서 만들고, 종료시켜야한다.
또, UserService에서 만든 Connection 오브젝트를 UserDao에서 사용하려면 DAO 메소드를 호출할 때마다 Connection 오브젝트를 파라미터로 전달해줘야 한다.

public void upgradeLevels() throws Exception { 
    (1) DB Connection 생성
    (2) 트랜잭션 시작
    try (
        (3) DAO 메소드 호출
        (4) 트랜잭션 커밋
    catch(Exception e) {
        (5) 트랜잭션 롤백
        throw e;
    finally {
        (6) DB Connection 종료
    }
}

public interface UserDao {
    void add(Connection c, User user);
    ...
    void update(Connection c, User user);
}

현시점 문제점

  1. DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가돼야 한다.
  2. Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다. (데이터 엑세스 기술에 의존적인 코드가됨)
    (예를들어 JPA나 하이버네이트로 UserDao의 구현 방식을 변경하려고 하면 Connection 대신 EntityManager나 Session 오브젝트를 UserDao 메소드가 전달받도록 해야 한다.)

해결방안 스프링 트랜잭션

1. 트랜잭션 동기화 : 데이터 접근 기술과 서비스 사이의 종속성 제거

트랜잭션 동기화

스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능을 지원하는 간단한 유틸리티 메소드를 제공하고 있다. TransactionSynchronizationManager

만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다.
반면에 upgradeLevels() 메소드에서처럼 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다. 이를 통해 이미 시작된 트랙잭션에 참여하는것이다.
덕분에 트랜잭션 적용 여부에 맞쳐 Dao 코드를 수정할 필요가 없다.
image

DataSource dataSource;

public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
}

public void upgradeLevels() throws Exception {
    // 트랜잭션 동기화 작업을 초기화
    TransactionSynchronizationManager.initSynchronization();
    // DB 커넥션을 생성하고 트랜잭션을 시작한다. 
    // 이후의 DAO 작업은 모두 여기서 시작한 트랜잭션 안에서 진행된다.
    Connection c = DataSourceUtils.getConnection(dataSource);
    c.setAutoCommit(false);

    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeUser(user);
            }
        }
        c.commit();
    } catch (Exception e) {
        // 예외가 발생하면 롤백한다.
        c.rollback();
        throw e;
    } finally {
        DataSourceUtils.releaseConnection(c, dataSource);
        // 스프링 유틸리티 메소드를 이용해 DB 커넥션을 안전하게 닫는다.
        TransactionSynchronizationManager.unbindResource(this.dataSource);
        TransactionSynchronizationManager.clearSynchronization();
    }
}

결과

jdbc 쿼리 동작할때 트랜잭션 동기화 매니저에 저장된 커넥션 가져와서 사용한다.
따라서 여러개의 update 쿼리를 모두 하나의 local transaction 으로 관리 가능하다.
또, Connection 파라미터도 제거 가능하다.

남은 문제점 : 데이터 접근기술에 종속적이다.

아직 Jdbc template의 DataSourceUtils 사용해서 connection 가져오거나 생성하기 때문에 요구사항이 바뀌게되어 JPA를 사용해야 한다면 코드 수정을 피할 수 없다.

2. 트랜잭션 추상화 : 데이터 액세스 기술에서 독립

트랜잭션 추상화

데이터 접근 기술이 달라도 가져오는 transaction 형태만 다를 뿐 기능은 같은 상황임을 인지하고 이를 추상화하자.
그렇게 하면 하위 시스템이 어떤 것인지 알지 못해도 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 있다.

이를 위해 스프링은 트랜잭션 서비스를 추상화 해두었다.
image
스프링은 다음과 같이 PlatformTransactionManger 인터페이스를 제공한다.
따라서 특정 데이터 접근 기술에 종속적이였던 문제를 해결할 수 있다.

예를들어 JDBC의 로컬 트랜잭션을 이용한다면 PlatformTransactionManager의 구현체인 DataSourceTransactionManager를 사용하면 되고, Hibernate를 사용한다면 HibernateTransactionManager을 사용하면 된다.

public void upgradeLevels() throws Exception {
    // 구현체인 DataSourceTransactionManager를 사용
    // 트랜잭션 시작과 커넥션 가져오기
    PlatformTransactionManager transactionManager
        = new DataSourceTransactionManager(dataSource);

    TransactionStatus status =
        transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLevel(user)) {
                upgradeUser(user);
            }
        }
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}

결론

다른 기술을 사용해야한다면? 코드는 변경할 필요없이 PlatformTransactionManager 구현체만 변경해주면 된다.
따라서 특정 데이터 접근 기술에 종속적이였던 문제를 해결할 수 있다.

예시 2) 메일 서비스 추상화

기존 상황

기존상황

실제 메일 서버에 계속해서 전송 요청을 보낸다면, 테스트로 인해 서버에 부담을 줄 수 있다.
그렇다면 테스트용 메일 서버를 이용하는 방법은 어떨까?
(테스트용 메일 서버는 메일 전송 요청은 받지만 실제로 메일을 보내진 않는다.)
(테스트 시에는 테스트 용 메일 서버로 메일 전송 요청을 보냈는지만 확인한다.)
운영시 : JavaMail을 직접 이용해서 동작하도록 한다.
테스트시 : 테스트용 JavaMail을 이용해서 특정 메소드가 호출되는지 여부만 확인한다.
image

JavaMail을 이용한 테스트의 문제점

JavaMail의 API는 위 방법을 적용할 수 없다.
JavaMail의 핵심 API에는 DataSource처럼 인터페이스로 만들어져서 구현을 바꿀수 있는게 없다.
JavaMail의 구현을 테스트용으로 바꿔치기하는 건 불가능하다고 볼 수밖에 없다.

스프링 MailSender

해결방안

스프링은 JavaMail에 대한 추상화 기능을 제공하고 있다.

public interface MailSender {
	void send(SimpleMailMessage simpleMailMessage) throw MailException;
    void send(SimpleMailMessage[] simpleMailMessage) throw MailException;
}

테스트용 MailSender 구현체를 만든다. 테스트가 수행될 때에는 JavaMail을 사용해서 메일을 보낼 필요가 없다.

public class DummyMailSender implements MailSender {
	public void send(SimpleMailMessage simpleMailMessage) throw MailException {
    
    }
    
    public void send(SimpleMailMessage[] simpleMailMessage) throw MailException {
    
    }	
}

테스트용 설정 파일은 JavaMailSenderImpl 대신 DummyMailSender를 빈으로 등록한다.
그러면 UserService 코드는 변경하지 않고 테스트 시에는 메일을 보내지 않도록 할 수 있다.
image
서비스 추상화란 이렇게 원활한 테스트만을 위해서도 충분히 가치가 있다.
기술이나 환경이 바뀔 가능성이 있음에도, JavaMail 처럼 확장이 불가능하게 설계 해놓은 API를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다.

@tmdcheol
Copy link

tmdcheol commented Aug 8, 2023

4장 예외


throws SQLException의 유무 차이

    public void deleteAll() throws SQLException {
        this.jdbcContext.executeSQL("delete from users");
    }

    public void deleteAll() {
        this.jdbcTemplate.update("delete from users");
    }

update라는 함수에서 런타임 예외를 터쳐버리기 (런타임 예외로 전환해서)
왜? 밑에서 설명해봅시다.


예외처리의 잘못된 습관

  1. catch로 잡기만 하고, 아무런 조치를 취하지 않는 경우
  2. 무분별한 throws

예외의 종류와 특징

  1. Error
    • 주로 VM에서 발생. OutOfMemoryError -> 해결 방법 없고, 애플리케이션에서 신경 쓰지 않아도 된다.
  2. Exception
    • unchecked exception - RuntimeException
    • checked exception - SQLException

unchecked exception은 예상하지 못했던 상황에서 발생하는 것이 아니기 때문에 catch, throws를 강요하지 않는다.


service 계층을 순수하게 가져가기 위해서는 SQLException을 제거해야한다.
언제 checked, unchecked를 써야할 지 알아야 한다.

예외 처리 방법

  1. 예외 복구
  2. 예외 처리 회피
  3. 예외 전환

예외 복구

해결해서 정상상태로 돌려놓기
예외 상황이 해결 가능한 경우가 있을 수 있다.
의도적으로 복구 시도하고, 안되면 포기하기

예외 처리 회피

단지 회피하는 것이 아니다.
의도가 분명하게, 콜백/템플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 분명히 책임을 지게 하거나, 자신을 사용하는 쪽으로 예외를 회피시키거나

예외 전환

  1. 적절한 의미부여를 해주기 위해서, 기술에 독립적이고 의미가 분명한 예외로 전환
    • 중첩 예외로 만들기
  2. 체크 예외를 언체크 예외로 바꾸기
    • 예외 처리를 강제를 없애서 코드 간결 (service까지 SQLException 올라가면 뭐해? 무의미해)
    • 비지니스적으로 의미가 있는 예외는 오히려 체크 예외로 만들어주자. (적절한 대응과, 복구작업이 필요)

예외처리 전략

런타임 예외의 보편화

  1. 초기에는 에러가 나면, 상황을 복구해주어야 하는 상황이 많았다.
  2. 서버환경에서는 요청이 독립적 -> 중단시키면 그만 -> 차라리 예외상황을 미리 파악하고 차단

1번과 같은 상황 -> 체크 예외가 적합
2번과 같은 상황 -> 런타임 예외로 던져버리는 것이 낫다.

그러나, api를 잘 숙지해서 예외의 종류와 원인, 활용방법을 잘 알고 런타임 예외를 사용해야겠죠?
런타임 예외는 처리를 강제하기 않기 때문에, 예외 상황을 잘 고려해야 한다.


애플리케이션 예외

출금 상황에서

  1. return 값을 다르게 해주어 if-else로 처리하거나
  2. 예외를 발생시키거나 -> 이 방법이 주로 사용

SQLException은 어디로 갔을까?

SQLException은 복구가 불가능한 상황이 대부분이라고 볼 수 있다.
JdbcTemplate template와 콜백 안에서 발생하는 SQLException은 DataAccessException 이라는 런타임 예외로 전환된다.
따라서 "throws SQLException" 이 사라진 것이다.
런타임 예외는 잡거나 던질 의무는 없다. 물론 복구 가능한 상황에서는 catch를 해서 복구할 수 있을 것이다.


호환성 없는 SQLException DB 에러정보

SQLException 안에 담긴 에러 코드는 다른 데이터베이스와 호환되지 않는다.
따라서 DB에 독립적인 에러정보를 가져오기 위해 getSQLState() 메소드로 상태정보를 제공한다. 표준 상태코드가 정의되어 있지만,
JDBC 드라이버에서 상태코드를 정확하게 만들어주지 않는다는 한계가 존재한다.
결론은 SQLException 만으로는 DB에 독립적인 유연한 코드를 작성하는 것은 불가능

DB 에러 코드 매핑을 통한 전환

결국 에러 코드 매핑파일을 이용해서, DB마다 다른 에러코드 일관성있게 Exception 클래스로 매핑한다
스프링은 DB 별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 테이블을 만들어서 이용한다.


DAO 인터페이스

결국 DB 구현체가 다를 때 구체화된 클래스를 만들고 인터페이스의 함수를 구현하는 방식을 택할 것이다.
하지만 구현체마다 정의된 에러클래스가 다른 한계가 또 다시 발생한다.
JPA -> PersisentException
Hibernate -> HibernateException

throws Exception으로 처리하는 것은 무책임한 선언이라고 하였다.
다행히 SQLException과 달리 JPA 같은 기술들은 런타임 예외를 뿜어 throws가 생략이 가능하다.

하지만, JDBC는 여전히 SQLException이다.

예외추상화와 DataAccessException 계층구조

그래서 스프링은 예외들을 추상화하여 DataAccessException을 제공하는 것이다.
에러코드에 따라 매핑된 에러클래스들은 DataAccessException의 하위 클래스이다.

ORM 기술에만 존재하는 에러는 어떻게 처리할까?
JDBC 를 이용해서 ORM에 있는 기능을 구현했다고 하면, DataAccessException의 계층구조를 파악해서
해당상황에 맞은 예외를 뿜으면 가능할 것이다.


생각해봅시다. 예외가 왜 있을까?

정리

  1. 예외를 잡아서 아무런 조취를 취하지 않거나 의미 없는 throws 선언을 남발하는 것은 위험하다.
  2. 예외는 복구하거나, 예외처리 오브젝트로 의도적으로 전달하거나, 적절한 예외로 전환해야한다.
  3. 좀 더 의미있는 예외로 변경하거나, 불필요한 catch/throws를 피하기 위해 런타임 예외로 포장하는 두 가지 방법의 예외 전환이 있다.
  4. 복구할 수 없는 예외는 가능한 한 빨리 런타임 예외로 전환하는 것이 바람직하다.
  5. 애플리케이션의 로직을 담기위한 예외는 체크 예외로 만든다.
  6. JDBC의 SQLException은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다.
  7. SQLException의 에러 코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환될 필요가 있다.
  8. 스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다.
  9. DAO를 데이터 엑세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화된 예외로 전환이 필요하다.

@kwonyonghyun
Copy link

kwonyonghyun commented Aug 8, 2023

AOP

현재까지 서비스 추상화를 통해 트랜잭션 경계설정의 기능중 많은 근본적인 문제를 해결했다. 6장에서는 AOP를 시용해 더욱 세련되고 깔끔한 방식으로 바꾼다.

트랜잭션 코드의 분리

public void upgradeLevels() throws Exception { 
    Transactionstatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        List<User> users = userDao.getAll(); 
				for (User user : users) {
            if (canUpgradeLevel(user)) { upgradeLevel(user); } 
        }
        this.transactionManager.commit(status); 
    } catch (Exception e) {
        this.transactionManager.rollback(status) ;
        throw e; 
    }
}

여기서 비즈니스 로직을 담당하는 코드와 트랜잭션 코드는 서로 주고받는 것도 없는, 완벽하게 독립적인 코드이다.

UserService 안에 트랜잭션 로직이 자리잡지 않도록, 아예 트랜잭션 코드가 존재하지 않는 것처럼 사라지게 할 수는 없을까?

DI적용을 위한 트랜잭션 분리

DI는 기본적으로 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접적으로 접근하는 것이다.

한번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용한다면?
(비즈니스 로직을 담는 구현 클래스, 트랜잭션의 경계설정이라는 책임을 맡는 구현 클래스)

public interface UserService { 
    void add(User user); 
    void upgradeLevels();
}
public class UserServicelmpl implements UserService { 
    UserDao userDao;
    MailSender mailSender;
    public void upgradeLevels() {

        List<User> users = userDao.getAll(); 
        for (User user : users) {
            if (canUpgradeLevel(user)) { 
                upgradeLevel(user);
            } 
        }
    }
}
public class UserServiceTx implements UserService { 
    UserService userService; PlatformTransactionManager transactionManager;
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager; 
    }

    public void setUserService(UserService userService) { 
        this.userService = userService;
    }

    public void add(User user) { 
        this.userService.add(user);
    }

    public void upgradeLevels() {
        Transactionstatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinitionO);
        try {
            userService.upgradeLevels();
            this.transactionManager.commit(status); 
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e; 
        }
    }
}

이와 같이, UserServiceTx는 비즈니스 로직을 전혀 갖지 않고 고스란히 다른 UserService 구현 오브젝트에 기능을 위임한다.

이렇게 되면, 클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때 먼저 트랜잭션을 담당하는 오브젝트가 사용돼서 트랜잭션에 관련된 작업을 진행해주고, 실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비즈니스 로직에 관련된 작업을 수행하게 된다.

고립된 단위 테스트

위 사진에서 볼 수 있듯이, UserService의 구현 클래스들이 동작하려면 세가지 타입의 의존 오브젝트가 필요하다.
테스트가 진행될 때 사전에 테스트를 위해 준비된 동작만 하도록 만든 두 개의 목 오브젝트에만 의존하는, 완벽하게 고립된 테스트 대상으로 만들 수 있다.

Mockito 프레임워크

Mockito라는 프레임워크는 사용하기도 편리하고, 코드도 직관적이라 퇴근 많은 인기를 끌고 있다.

  • mock() : 목 오브젝트를 생성하기 위한 메소드로 스태틱 임포트를 사용해 로컬 메소드 처럼 호출하게 하면 편리하다.
  • when() : mock으로 생성된 인스턴스는 아직 아무런 동작도 하지 않는다. mock 객체의 메소드에 필요한 mock을 주입하기 위해서는 when 메소드로 설정해주어야 한다.
  • verify() : 목 객체의 메소드가 호출되었는지 확인할 수 있다.

다이내믹 프록시와 팩토리 빈

이전의 코드를 보면 트랜잭션이라는 기능은 사용자 관리 비즈니스 로직과는 성격이 다르기 때문에 아예 그 적용 사실 자체를 밖으로 분리한 UserServiceTx라는 클래스를 만들었고, UserServiceImpl에는 트랜잭션 관련 코드가 하나도 남지 않게 되었다.이처럼, 부가기능 외의 나머지 모든 기능은 원래 핵심기능을 가진 클래스로 위임해줘야 한다. 핵심기능은 부가기능을 가진 클래스의 존재 자체를 모른다.
문제는 이렇게 구성했더라도 클라이언트가 핵심기능을 가진 클래스를 직접 사용해버리면 부가기능이 적용될 기회가 없다는 점이다. 그래서 부가기능은 마치 자신이 핵심 기능을 가진 클래스인 것처럼 꾸며서, 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다.

클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게 하고, 부가기능 자신도 같은 인터페이스를 구현한 뒤에 자신이 그 사이에 끼어들어야 한다.

이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시(proxy)라고 부른다. 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃 또는 실체라고 부른다.

다이내믹 프록시

자바에는 java.lang.reflect 패키지 안에 프록시를 손쉽게 만들 수 있도록 지원해주는 클래스들이 있다.

프록시의 구성과 프록시 작성의 문제점

  • 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거롭다. 부가기능이 필요 없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 한다.
  • 부가기능 코드가 중복될 가능성이 만다는 점이다. 트랜잭션은 DB를 사용하는 대부분의 로직에 적용될 필요가 있다. 메소드가 많아지고 트랜잭션 적용의 비율이 높아지면 트랜잭션 기능을 제공하는 유사한 코드가 여러 메소드에 중복돼서 나타날 것이다.

리플렉션
클래스 오브젝트를 이용하면 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있다.

Method lengthMethod = String.class.getMethod("length");
int length = lengthMethod.invoke(name); // int length = name.length();

invoke() 메소드는 메소드를 실행시킬 대상 오브젝트와 파라미터 목록을 받아서 메소드를 호출한 뒤에 그 결과를 Object 타입으로 돌려준다.

다이내믹 프록시 적용

다이나믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트다. 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다. 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문이다.
리플렉션으로 메소드와 파라미터 정보를 모두 갖고 있으므로 타깃 오브젝트의 메소드를 호출하게 할 수도 있다.

public class UppercaseHandler implements InvocationHandler {
    Hello target;

    public UppercaseHandler(Hello target) { 
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object!] args) throws Throwable {
        String ret = (String)method.invoke(target, args);
        return ret.toUpperCase();
    }
}
Hello proxiedHello = (Hello)Proxy.newProxyInstance( 
    getClass().getClassLoader(),
    new Class[] {Hello.class}, // 구현할 인터페이스
    new UppercaseHandler (new HelloTarget()));

프록시 팩터리 빈 방식의 한계: 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 불가능하다.

TransactionHandler의 중복을 없애고 모든 타깃에 적용 가능한 싱글톤 빈으로 만들어서 적용할 수는 없을까?

스프링의 프록시 팩토리 빈

ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다.

  • ProxyFactoryBean : 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈

MethodInterceptor과 InvocationHandler의 차이점

  • InvocationHandler의 invoke() 메소드 : 타깃 오브젝트에 대한 정보를 제공하지 않는다. 따라서 타깃은 InvocationHandler를 구현한 클래스가 직접 알고 있어야 한다.
  • MethodInterceptor의 invoke() 메소드 : ProxyFactoryBean으로 부터 타깃 오브젝트에 대한 정보까지 함께 제공받는다.
public void simpleProxy() {
    Hello proxiedHello = (Hello)Proxy.newProxyInstance( //  JDK 다아내믹 프록시 생성
        getClass().getClassLoader(),
        new Class[] {Hello.class},
        new UppercaseHandler(new HelloTarget())
    );
}

public void proxyFactoryBean() {
    ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(new HelloTarget());    //  타깃 설정
    pfBean.addAdvice(new UppercaseAdvice());    //  부가기능을 담은 어드바이스를 추가한다. 여러 개를 추가할 수도 있다.

    Hello proxiedHello = (Hello) pfBean.getObject();    //  FactoryBean이므로 getObject()로 생성된 프록시를 가져온다.
    System.out.println(proxiedHello.sayHello("JaeDoo"));
    System.out.println(proxiedHello.sayHi("JaeDoo"));
    System.out.println(proxiedHello.sayThankYou("JaeDoo"));
}

static class UppercaseAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        /*
            리플렉션의 Method와 달리 MethodInvocation은 
            메소드 정보와 함께 타깃 오브젝트를 알고 있기 때문에
            메소드 실행 시 타깃 오브젝트를 전달할 필요가 없다.
        */
        String ret = (String)invocation.proceed();
        return ret.toUpperCase();   //  부가기능 적용
    }
}

public interface Hello {            //  타깃과 프록시가 구현할 인터페이스
    String sayHello(String name);
    String sayHi(String name);
    String sayThankYou(String name);
}

static class HelloTarget implements Hello { //  타깃 클래스

    @Override
    public String sayHello(String name) { return "Hello " + name;}

    @Override
    public String sayHi(String name) { return "Hi " + name;}

    @Override
    public String sayThankYou(String name) {return "Thank You " + name;}
}

어드바이스: 타깃이 필요 없는 순수한 부가기능

  • 부가기능을 제공

포인트컷: 부가기능 적용 대상 메소드 선정 방법

  • 메소드 선정 알고리즘, 포인트컷 인터페이스를 구현해서 만든다

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