이번에 새로운 프로젝트를 시작하게 되었는데 로그인에 대한 구현이 필요해서 다음과 같이 Spring Security를 사용해서 JWT 방식으로 로그인을 구현해보려고한다.
2. JWT란?
JWT는 JSON Web Token으로 Header, Payload, Signature등으로 나눈 정보를 Base64 URL-safe Encode을 통해서 인코딩해 직렬화 한 방식이다. 사실, 다른 블로그에서 설명을 잘해놨기 때문에 이 정도만 알고 있으면 좋을것 같다. 참고자료
3. 인증방식
인증 방식은 우선 AccessToken과 RefreshToken을 사용해서 구현할 예정이다. 그래서 다음 형식을 지닌다. 사실 이 부분은 다른 블로그에도 잘 설명이 되어있기 때문에 참고링크를 통해서 확인해보면 좋을것 같다.
1. 계기
이번에 새로운 프로젝트를 시작하게 되었는데 로그인에 대한 구현이 필요해서 다음과 같이 Spring Security를 사용해서 JWT 방식으로 로그인을 구현해보려고한다.
2. JWT란?
JWT는 JSON Web Token으로 Header, Payload, Signature등으로 나눈 정보를 Base64 URL-safe Encode을 통해서 인코딩해 직렬화 한 방식이다. 사실, 다른 블로그에서 설명을 잘해놨기 때문에 이 정도만 알고 있으면 좋을것 같다. 참고자료
3. 인증방식
인증 방식은 우선 AccessToken과 RefreshToken을 사용해서 구현할 예정이다. 그래서 다음 형식을 지닌다. 사실 이 부분은 다른 블로그에도 잘 설명이 되어있기 때문에 참고링크를 통해서 확인해보면 좋을것 같다.
다음과 같은 로직으로 작동하게 만드려고한다. 여기서 세부적으로 설명해야할 부분들을 설명하도록 하겠다.
3-1. Refresh Token의 사용이유
이번 프로젝트에서 고민했던 것이 Refresh 토큰을 사용하는 부분에서 고민이 많았다.
여러가지 자료들을 참고했지만, 살짝씩 다른 말들뿐이었다. 그래서 소신대로 해보자라고 생각하고 다음처럼 구성을 했다.
우선 설명하기 전에 Refresh Token왜 생겨났냐?에 대한 이야기를 해보려고한다. 우선 각 토큰에는 만료시간이라는 것이 존재해, 이 만료시간이 끝난다면, 토큰은 더 이상 사용하지 못한다는 점이 존재한다.
그래서, 보안을 위해서 보통 Access Token의 만료시간을 짧게 주고 자주 재발급 받는 형식을 사용을한다. 하지만 여기에서 문제점이 생긴다. Access Token만 존재한다면 재발급 받는 방법은, 다시 로그인을 하는 방법 뿐이다.
사용자들은 자주 로그인 하는것을 싫어하기 때문에 UX적인 측면을 고려하지 못할 수도 있다. 그렇다고, 만료시간을 길게 잡는다면 만료시간이 길어져서 공격자가 토큰을 탈취하게 된다면, 공격자인지 사용자인지 서버는 모르게되고 사용자의 정보는 탈취당하게 된다.
그래서 Refresh Token이 등장하게 되었다. Refresh Token과 Access Token을 같이 로그인을 할때 발급을 하는 방식을 사용하려고 한다.
이런 Refresh Token을 관리하는 방식도 여러가지가 있다 크게는 다음과 같이 운영하는것 같다.
Local Storage에 프론트가 저장을 한다.
Cookie를 통해서 Refresh Token을 저장한다 (HttpOnly 속성을 넣어서)
서버가 Refresh Token을 관리한다.
결론적으로
Access Token의 만료시간이 길면, 탈취당하고 공격자에 의해 이용당할 수 있음
Access Token의 만료시간이 짧으면, UX 측면에서 사용자는 짧은 시간주기로 계속해서 재로그인을 해야함
3-2 Front에서 Local storage에 저장해서 사용하는 방식
XSS 공격등으로, 탈취당할 문제점이 존재합니다. 하지만, 제일 편한 방식입니다. 또한, LocalStorage는 자바스크립트로도 접근이 가능합니다.
window.localStorage.getItem();
3-3 Front에서 Cookie
HttpOnly나 Secure이나 이런 정보를 넣어 LocalStorage 보다는 안전하게 저장을 할 수 있습니다. 하지만, 이것도 CSRF공격등으로 탈취당할 수 있습니다.
3-4 Server에서 저장하는 방식
Session 또는 DB에 저장할 수 있습니다. 하지만, 이 방식은 Server에 무리를 일으킨다는 단점이 존재합니다. 그래서 현재는 Redis같은 In-Memory DB 방식을 사용해서 사용을 합니다.
4. 프로젝트에 결정한 방식
프로젝트에서는 간단하게 Cookie방식을 통한 방법을 고려하기로 했습니다. 당연히 Security는 안전하게 할 수록 좋지만, 어떤 방식이던지 취약점은 존재하기 때문입니다. 100% 막을 수 있는 보안방법은 없습니다. 또한, 각 방식마다 장단점이 존재하기 때문에 결론적으로는 Cookie에 HttpOnly와 Secure Flag를 추가해서 배포하기로 마음먹었습니다.
5. 아니 그래서 HttpOnly랑 Secure가 뭔데?
계속해서 HttpOnly랑 Secure를 넣어서 안전하게 사용할것이다라고 하는데 그래서 이게 뭔지에 대한 설명이 적은것 같다.
우선, HttpOnly는 자바스크립트로 쿠키를 조회하는 것을 막는 옵션이라고 보면 좋다. 그리고, Secure는 HTTPS로 통신하는 경우에만 쿠키를 서버로 전송하는 옵션이라고 보면 좋다.
다른 블로그나 참고자료에서는 이 부분에 대해서 설명한 곳이 별로 없거니와, 사람마다 차이점이 존재하기 때문에 그래서 소신대로 라는 말을 붙여서 제목에 의미를 더했다.
검증 방법은 다음과 같은 종류를 생각했다.
첫 번째로 Access Token에 문제가 있는 경우는 다음과 같은 경우가 있다고 판단하였습니다.
Authorized 헤더에 Access Token 값이 없거나 헤더에 대한 정보가 없는 경우
Access Token이 유효하지 않은 토큰인 경우
Access Token의 시간이 만료된 경우
마지막으로는 Refresh Token에 대해서 문제가 있는 상황은 다음과 같은 경우를 생각했습니다.
Access Token이 만료되지 않았을 때 Refresh Token을 사용해 재발급 받는 경우는 해당 공격자가 탈취를 한경우라고 판단하여 두 토큰을 모두 만료시키는 방식으로 하였습니다.
Refresh Token이 Cookie에 존재하지 않는경우
Refresh Token으로 DB에 유저정보를 검색하는데, 이 토큰을 가지고 있는 사용자가 없는 경우
Refresh Token의 유효시간이 만료된 경우
Refresh Token이 유효하지 않은 토큰인 경우
다음과 같은 상황을 검증하려고 합니다.
7. 마지막으로
이제 다음 부터는 직접 Spring Security를 써서 어떻게 구축을 했는지를 확인하려고 합니다. 다음과 같은 로직으로 작동하게 만드려고한다. 여기서 세부적으로 설명해야할 부분들을 설명하도록 하겠다.
3-1. Refresh Token의 사용이유
이번 프로젝트에서 고민했던 것이 Refresh 토큰을 사용하는 부분에서 고민이 많았다.
여러가지 자료들을 참고했지만, 살짝씩 다른 말들뿐이었다. 그래서 소신대로 해보자라고 생각하고 다음처럼 구성을 했다.
우선 설명하기 전에 Refresh Token왜 생겨났냐?에 대한 이야기를 해보려고한다. 우선 각 토큰에는 만료시간이라는 것이 존재해, 이 만료시간이 끝난다면, 토큰은 더 이상 사용하지 못한다는 점이 존재한다.
그래서, 보안을 위해서 보통 Access Token의 만료시간을 짧게 주고 자주 재발급 받는 형식을 사용을한다. 하지만 여기에서 문제점이 생긴다. Access Token만 존재한다면 재발급 받는 방법은, 다시 로그인을 하는 방법 뿐이다.
사용자들은 자주 로그인 하는것을 싫어하기 때문에 UX적인 측면을 고려하지 못할 수도 있다. 그렇다고, 만료시간을 길게 잡는다면 만료시간이 길어져서 공격자가 토큰을 탈취하게 된다면, 공격자인지 사용자인지 서버는 모르게되고 사용자의 정보는 탈취당하게 된다.
그래서 Refresh Token이 등장하게 되었다. Refresh Token과 Access Token을 같이 로그인을 할때 발급을 하는 방식을 사용하려고 한다.
이런 Refresh Token을 관리하는 방식도 여러가지가 있다 크게는 다음과 같이 운영하는것 같다.
Local Storage에 프론트가 저장을 한다.
Cookie를 통해서 Refresh Token을 저장한다 (HttpOnly 속성을 넣어서)
서버가 Refresh Token을 관리한다.
결론적으로
Access Token의 만료시간이 길면, 탈취당하고 공격자에 의해 이용당할 수 있음
Access Token의 만료시간이 짧으면, UX 측면에서 사용자는 짧은 시간주기로 계속해서 재로그인을 해야함
3-2 Front에서 Local storage에 저장해서 사용하는 방식
XSS 공격등으로, 탈취당할 문제점이 존재합니다. 하지만, 제일 편한 방식입니다. 또한, LocalStorage는 자바스크립트로도 접근이 가능합니다.
window.localStorage.getItem();
3-3 Front에서 Cookie
HttpOnly나 Secure이나 이런 정보를 넣어 LocalStorage 보다는 안전하게 저장을 할 수 있습니다. 하지만, 이것도 CSRF공격등으로 탈취당할 수 있습니다.
3-4 Server에서 저장하는 방식
Session 또는 DB에 저장할 수 있습니다. 하지만, 이 방식은 Server에 무리를 일으킨다는 단점이 존재합니다. 그래서 현재는 Redis같은 In-Memory DB 방식을 사용해서 사용을 합니다.
4. 프로젝트에 결정한 방식
프로젝트에서는 간단하게 Cookie방식을 통한 방법을 고려하기로 했습니다. 당연히 Security는 안전하게 할 수록 좋지만, 어떤 방식이던지 취약점은 존재하기 때문입니다. 100% 막을 수 있는 보안방법은 없습니다. 또한, 각 방식마다 장단점이 존재하기 때문에 결론적으로는 Cookie에 HttpOnly와 Secure Flag를 추가해서 배포하기로 마음먹었습니다.
5. 아니 그래서 HttpOnly랑 Secure가 뭔데?
계속해서 HttpOnly랑 Secure를 넣어서 안전하게 사용할것이다라고 하는데 그래서 이게 뭔지에 대한 설명이 적은것 같다.
우선, HttpOnly는 자바스크립트로 쿠키를 조회하는 것을 막는 옵션이라고 보면 좋다. 그리고, Secure는 HTTPS로 통신하는 경우에만 쿠키를 서버로 전송하는 옵션이라고 보면 좋다.
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User("gyumee", "박성철", "springno1");
이제 getCount() 에 대한 테스트 메서드를 작성해보겠습니다.
@Testpublicvoidcount()throws SQLException{
ApplicationContext context = new GenricXmlApplicationContext("applicationContex.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user1 = new User("gyumee", "박성철", "springno1");
User user2 = new User("hachan", "유해찬", "springno2");
User user3 = new User("chayoo", "찬해유", "springno3");
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
assertThat(dao.getCount(), is(1));
dao.add(user2);
assertThat(dao.getCount(), is(2));
dao.add(user3);
assertThat(dao.getCount(), is(3));
}
이렇게 작성을 하면 별 문제 없이 실행이 됩니다.
하지만 이는 특정한 테스트 메서드의 실행 순서를 보장해주지는 안씁니다.
만약, 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것입니다.
그래서 우리는 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야합니다.
addAndGet() 테스트 보완
이제 addAndGet() 을 보완해봅시다. 이는 우리는 현재 add() 는 여러번 하면서 검증을 완료하였지만 get() 에 대해서는 id를 조건으로만 찾았기 때문에 이는 부족한 감이 있습니다. 그래서 검증이 부족한 감이 있습니다.
이를 User의 id를 파라미터로 전달하여 get()을 실행하게 할 것입니다.
그래서 get()에 대한 테스트 기느이 보완된 addAndGet() 테스트코드를 살펴봅시다.
@TestpublicvoidaddAndGet()throws SQLExeption{
/...
UserDao dao = context.getBean("userDao", UserDao.class);
User user1 = new User("haechan", "유해찬", "spring1");
User user2 = new User("chanyoo", "찬해유", "spring2");
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
dao.add(user2);
assertThat(dao.getCount(), is(2));
User userget1 = dao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName());
assertThat(userget1.getPassword(), is(user1.getPassword());
User userget2 = dao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName());
assertThat(userget2.getPassword(), is(user2.getPassword());
}
이를 통해서 get() 메서드에 대해서 더욱 정확히 확인할 수 있습니다.
get() 예외조건에 대한 테스트
get() 메서드에 전달된 id 값에 해당하는 사용자 정보가 없으면 어떻게 될지 생각해봅시다.
우리는 이를 2가지 방법으로 처리할 수 있습니다.
null과 같은 값을 리턴하는 방식
id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 방식입니다.
우리는 후자를 사용할 것입니다.
스프링에서는 데이터 액세스 예외 클래스가 있습니다. 우리는 그 예외인 EmptyResultDataAccessException 예외를 사용할 것입니다.
만약, get()을 실행 했을 때 결과에 아무것도 없으면 예외를 던지게 하면됩니다. 만약 예외가 던져지게 되면 테스트 메서드의 실행은 중단되고 실패합니다.
하지만 우리는 현재 EmptyResultDsataAccessException 이라는 예외가 던져져야 우리는 테스트가 성공을 했구나? 라는 것을 알 수 있습니다. 이런 특별한 상황에는 assertThat() 을 사용할 수 없습니다.
그래서 이런경우에는 JUnit의 기능을 사용할 수 있습니다. 확인해 봅시다.
@Test(excepted=EmptyResultDataAccessException.class)publicvoidgetUserFailure()throws SQLException{
ApplicationContext context = new GenericXmlApplicationContext("appplicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
dao.deleteAll();
assertThat(dao.getCount(),is(0));
dao.get("unknown_id");
}
@Test 에서 excepted 엘리먼트를 사용해서 우리는 기대하는 예외클래스를 넣어주면 확이니 가능합니다.
만약, excepted 라는 엘리먼트를 추가하면 예외가 던져지면 성공이고 던져지지 않으면 실패입니다.
테스트를 성공시키기 위한 코드의 수정
이제부터 우리는 get() 메서드 코드를 수정해서 이 테스트가 성공하도록 할 것입니다.
다음과 같이 작성을 하면 만들 수 잇습니다.
// 데이터를 찾지 못하면 예외를 발생시키도록 수정한 get() 메서드public User get(String id)throws SQLException{
/...
ResultSet rs = ps.excuteQuery();
User user = null;
if(rs.next()){
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
if(user == null) thrownew EmptyResultDataAccessException(1);
return user;
}
}
이런식으로 get() 메서드에서 데이터가 없을 때 예외를 발생시키면 테스트는 성공할 것입니다.
포괄적인 테스트
사실 우리가 작성한 코드들은 간단해서 테스트가 없어도 문제를 살펴볼 수 잇습니다.
하지만 이런식으로 DAO의 메서드에 대해서 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용합니다.
또한, 여러가지 이점이 존재합니다.
개발자들은 이상한 습성이 있습니다. 바로 성공하는 테스트만 골라서 만드는 것입니다. 하지만 이러한 것은 큰 문제점입니다. 스프링의 창시자도 ”항상 네거티브 테스트를 먼저 만들라” 라는 조언을 하고 잇습니다. 그렇기 때문에 부정적인 케이스 부터 먼저 만드는 습관을 들이는 것이 좋습니다.
테스트가 이끄는 개발
우리는 get() 메서드의 예외 테스트를 만들면서 UserDao 코드를 수정을 계속해서 반복하였습니다. 이는 테스트를 통해서 우리는 개발을 진행하였습니다.
물론 테스트할 코드도 없이 테스트 코드를 만드는 것은 이상하지만 이러한 순서를 따라서 개발하는 TDD 방식의 개발도 존재합니다, 우리는 이런 개발 방법을 적극적으로 권장합니다.=
기능설계를 위한 테스트
우리는 가장 먼저 존재하지 않는 id로 get() 메서드를 실행한다면 특정한 예외가 던져져야한다. 라는 방식으로 만들어야 할 기능을 결정하였습니다.
하지만 테스트할 코드도 없는데 어떻게 테스트할까요? 이는 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능합니다.
왜 이렇게 작동하는지에 대해서는 JUnit 이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식을 살펴봅시다.
테스트 클래스에서 @Test 가 붙은 public 이고 void 형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.
테스트 클래스의 오브젝트를 하나 만든다.
@Before 가 붙은 메서드가 있으면 실행한다.
@Test 가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해둔다.
@After 가 붙은 메서드가 있으면 실행합니다.
나머지 테스트 메서드에 대해 2~5번을 반복합니다.
모든 테스트의 결과를 종합해서 돌려줍니다.
세부적으로는 더 복잡하지만 크게보면 다음과 같습ㄴ디ㅏ.
그렇기 때문에 @Before 이나 @After 메서드를 자동으로 실행합니다.
하지만, @Before 나 @After 메서드를 테스트 메서드에서 직접 호출하지 않기 때문에 이를 인스턴스 변수를 이용해서 저장하여 테스트 메서드에서 사용하게 만들면 됩니다.
이러한 테스트 메서드를 실행할때마다 새로운 오브젝트를 만들고 있습니다. JUnit은 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서 매번 새로운 오브젝트들을 만들게 한것이 그 이유입니다
그래서 우리는 인스턴스 변수를 부담없이 사용을 할 수 있습니다. 다음 텍스트 메서드가 실행 될 때는 새로운 오브젝트가 만들어져서 다 초기화 될 것이기 때문입니다.
만약, 테스트 메서드의 일부에서만 공통적으로 사용되는 코드가 있다면 어떻게 해야할까요?
이때는 @Before 보다는 일반적으로 우리가 사용하는 메서드 분리 방식을 이용하는 것이 바람직 합니다.
픽스처
픽스처(fixture)는 테스트를 수행하는 데 필요한 정보나 오브젝트를 말합니다.
이는 여러 테스트에서 반복되어 사용되기 때문에 @Before 를 통해서 메서드를 생성해두면 편합니다.
우리가 짠 코드를 본다면 현재 getUserFailure() 라는 테스트를 제외하고는 User 에 대한 오브젝트가 사용이 되고 있습니다. 우리는 일부에서만 공통적으로 사용하기 때문에 메서드 분리 방식을 이용한다고 생각하지만 다른 테스트들은 거의다 User 에 대해서 사용할 것이기 때문에 @Before 를 통해서 빼주도록 합니다.
그래서 다음과 같은 코드를 생성할 수 있습니다.
publicclassUserDaoTest{
private UserDao dao;
private User user1;
private User user2;
private User user3;
@BeforepublicvoidsetUp(){
/...
this.user1 = new User("haechan", "유해찬", "spring1");
this.user2 = new User("chanyoo", "찬유해", "spring2");
this.user3 = new User("yoohae", "해유찬", "spring3");
}
}
이런식으로 ApplicationContext를 사용해서 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 하기 때문에 우리는 의존관계 검색이라고 합니다.
이렇게 만들어진 의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 가집니다. 또한, IoC 원칙에도 잘 들어맞습니다. 하지만 방법만 다릅니다.
결론적으로 어떤것이 더 낫냐라고 본다면 의존관계 주입이 더 깔끔합니다. 또한, 다른 단점들도 존재합니다.
의존관계 검색을 사용하면 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타나서 성격이 다른 오브젝트에 의존하게 되어 바람직하지 않습니다.
사용자에 대한 DB 정보를 어떻게 가져올건가에 집중하는 UserDao 에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여있으면 어색합니다.
그래서 의존관계 주입 방법을 사용하지만 의존관계 검색 방식을 사용해야할 떄도 있습니다.
앞서서 만들었던 UserDaoTest를 다시 봅시다.
publicclassUserDaoTest{
publicstaticvoidmain(String[] args)throws ClassNotFoundException,
SQLException{
ApplicationContext context =
new AnnotationCongifApplicationContext(DaoFactory.class);
UserDao dao = context.getBean("userDao", UserDao.class);
}
}
확인해보면 의존관계 검색 방식을 사용한 것을 볼 수 있습니다. 이러한 경우는 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 합니다.
왜냐하면 main() 메서드는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문에 우리는 서블릿이라는 것을 사용하여 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다.
우리가 사용하는 의존관계 검색과 의존관계 주입은 차이점이 있습니다.
의존관계 검색 방식은 검색하는 오브젝트는 자신이 스프링의 빈일 필요는 없습니다.
위에 코드를 살펴보면 UserDao 는 굳이 getBean()이 아닌 new UserDao() 해서 만들어도 사용해도 됩니다.
의존관계 주입에서는 UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao 도 반드시 컨테이너가 만드는 빈 오브젝트여야 합니다.
DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말아야합니다.
의존관계 주입의 응용
런타임 시에 사용 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점은 무엇인지 확인해봅시다.
우리가 앞에서 오브젝트 팩토리로 구현을 하였는데 이는 DI 방식을 구현한 것입니다. 이러한 DI의 장점을 그대로 얻을 수 있습니다.
코드에는 런타임 클래스에 대한 의존관계가 나타나지 않습니다.
인터페이스를 통해 결합도가 낮은 코드를 만들어 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않습니다.
변경을 통한 다양한 확장 방법에는 자유롭습니다.
우리는 이러한 DI 방식을 스프링을 사용하면 99% 혜택을 볼 수 있습니다. 이러한 DI 방식의 여러가지 응용사례를 살펴봅시다.
기능 구현의 교환
실제 운영에 사용할 데이터베이스는 매우 중요한 자원입니다. 이러한 데이터 베이스는 항상 부하가 많이 가해집니다. 그래서 개발 중에는 절대로 사용하지 말아야합니다.
그래서 이제 개발자의 로컬 DB로 사용한다고 생각해봅시다. 우리는 로컬 DB에 대한 기능이 있는 LocalDBConnectionMaker 라는 클래스를 만들고 제일 처음에 만들었던 초난감 DAO와 같이 사용하고 있다고 생각해봅시다.
이렇게 Local에서 사용을 하고나서 다시 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야한다고 생각해봅시다.
우리는 일단 초난감 DAO방식을 사용하고 있기 때문에 모든 DAO들에는 new LocalDBConnectionMaker() 라는 코드를 사용해서 LocalDBConnectionMaker에 의존을 하고 있을 것입니다.
만약 이제 배포를 해야한다고 생각해봅시다. 그러면 LocalDBConnectionMaker 에서 ProductionDBConnectionMaker 라는 클래스로 변경해줘야 한다고 생각해봅시다. 만약 DAO가 1000개라면 1000개의 코드를 모두 수정해야합니다. 이러한 방식은 매우 끔찍합니다.
만약, 초난감 DAO가 아닌 DI 방식을 적용해서 만들었다고 생각해봅시다.
그러면 DAO는 생성 시점에서 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공을 받습니다.
컨테이너에는 구체적으로 사용할 클래스 이름이 들어있습니다.
그래서 @Configuration이 붙은 DaoFactory를 사용한다고 한다면 개발자 PC에서 DaoFactory 코드를 이전에 만든 방식처럼 만들면 됩니다.
이런식으로 DB가 개발용 배포용이 나눠져있거나 다른 DB들을 여러개 사용할 경우 이런식으로 DI 컨테이너를 만들어서 모든 DAO가 사용할 수 있도록 DI 해줄 수 있습니다.
부가기능 추가
만약에 우리가 DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 생각해봅시다.
첫 번째 방법으로는 그냥 단순하게 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣을 수 있습니다. 하지만 이런 방식은 너무 비효율적 입니다.
두 번째 방법으로는 DI 컨테이너에서 사용할 수 있는 방법입니다. DI에서는 매우 간단하게 하결할 수 있습니다. 그냥 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면 됩니다.
DI 개념을 쉽게 응용한다면 기존코드를 수정하지 않고 그냥 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의하면 가능합니다. 우리는 CountingConnectionMaker라는 클래스를 리스트 1-30과 같이 만듭니다. 또한, 이 클래스는 ConnectionMaker 인터페이스를 구현해서 만든다고 합니다.
CountingConnectionMaker 클래스는 현재 ConnectionMaker 를 인터페이스를 implement해서 구현을 하였지만 직접 DB커넥션을 만들지는 않았습니다.
대신 DAO가 DB를 가져오는 makeConnection()에서 DB 연결횟수 카운터를 증가시킵니다.
CountingConnectionMaker 는 DB 연결횟수 카운팅이 관심사여서 이를 끝낸 뒤에는 DB 커넥션이 정의되어 있는 realConnectionMaker 에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려줍니다.
이러한 것은 CountingConnectionMaker도 DI를 받고 이에 대한 오브젝트가 DI를 받을 오브젝트도 ConnectionMaker 인터페이스를 구현한 오브젝트입니다. 결론적으로 우리가 사용하는 AConnectionMaker 같은 실제 DB 커넥션을 돌려주는 클래스의 오브젝트를 받습ㄴ디ㅏ.
이는 CountingConnectionMaker → ConnectionMaker → UserDao 로 가는 것을 볼 수 있습니다.
AConnectionMaker 가 구현하고 있는 ConnectionMaker 라는 인터페이스에만 의존하고 있기 때문에 만약 ConnectionMaker 를 implement 하고 있는 어떤 클래스라도 바꿔치기가 가능합니다. 그래서 CountingConnectionMaker 가 사용이 가능한 것입니다.
우리는 이것만 사용하면 안됩니다. CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 AConnectionMaker 를 호출하도록 만들어야 합니다. 이럴 때도 DI를 사용하면 됩니다.
다음과 같이 CountingDaoFactory 라는 컨테이너를 만들어서 사용할 수 있게 만들 수 있습니다.