[스프링 핵심 원리] 스프링의 핵심 원리
예제를 통한 스프링 핵심 원리(DI)의 이해
※ 코드는 강의에서 사용된 것과 다릅니다.
<a href = https://github.com/jaewon-ju/Learning_Spring>jaewon-ju Github Address</a>
실제 프로젝트를 설계해보면서 왜 스프링이 필요한지, 스프링의 핵심 원리는 무엇인지 이해해보자.
✏️ 들어가기 전에..
JUnit이란, 테스트를 위한 자바용 프레임워크이다.
비즈니스 로직을 구현한 뒤에, JUnit 프레임워크를 사용하여 반드시 테스트를 해봐야 한다.
- @Test 어노테이션을 통해 테스트 메소드임을 선언할 수 있다.
- @BeforeEach 어노테이션을 통해 각 테스트 실행 전에 실행될 메소드를 선언할 수 있다.
Assertions.assertThat(A).isEqualTo(B)를 통해 A와 B가 같은지 확인하고 test 성공 여부를 알 수 있다.
✏️ 비즈니스 요구사항
학원용 게시판 프로그램을 순수한 자바 코드로 설계한다.
■ 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 학생 회원과, 교사 회원이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
■ 게시판 이용
- 회원은 게시판을 이용할 수 있다.
- 회원 자격에 따라 게시판에 글을 쓸 수 있다.
- 학생 회원은 글을 읽는 기능만 가능하게 설계하고 싶다.
- 하지만, 학생 회원의 게시판 이용 범위를 확실하게 결정하지 못했다. (미확정)
요구사항을 보면 회원 데이터, 이용 범위 등은 지금 결정하기 어렵다. 이러한 세부사항이 결정될 때까지 개발을 미룰 수는 없다! ➜ 다형성을 이용한다.
✏️ 회원 도메인 설계
1
2
3
// Member는 id, name, position 필드와 Getter/Setter를 가지는 클래스
// Position은 열거형으로 STUDENT, TEACHER 2가지 종류가 있다.
Member 클래스 구현 생략
1
2
3
// MemberRepository 인터페이스는 save, findById를 추상메소드로 갖고있다.
// MemoryMemberRepository 구현 객체는 save, findById 메소드를 오버라이딩했다.
MemoryRepository 인터페이스, MemoryMemberRepository 클래스 구현 생략
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//MemberServiceImpl 구현 객체
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.add(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
■ 설계의 문제점
MemberServiceImpl 객체가 MemberRepository 뿐만 아니라, MemoryMemberRepository에도 의존하고 있음
즉, 구현객체가 인터페이스뿐만 아니라 구현객체에도 의존하고 있음 ➜ DIP 위반
메모리 저장소에서 DB 저장소로 바꾸려면, 클라이언트인 MemberServiceImpl을 변경해야함 ➜ OCP 위반
✏️ 게시판 도메인 설계
게시판에서 게시글을 작성하는 과정
| 순서| 실행 주체 | 기능 | | - | - | - | | 1 | BoardApp | member.join() 으로 회원 가입 -> 학생 회원이라 가정| | 2 | BoardApp | boardService.posting() 호출 | | 3 | BoardServiceImpl | 저장소에서 해당 member 정보 가져옴 | | 4 | BoardServiceImpl | boardPolicy.returnAuthority() 호출| | 5 | ReadOnly | 학생 회원이므로 false 반환 | | 6 | BoardServiceImpl | authority가 false이므로 제목과 내용 인수는 unavailable로 설정| | 6 | BoardServiceImpl | Post 타입으로 게시글 반환 | | 7 | BoardApp | 2번의 호출로 받은 게시글 출력 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 게시판 도메인 BoardApp
public class BoardApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
BoardService boardService = new BoardServiceImpl();
Member member = new Member(1L, "studentA", Position.STUDENT);
memberService.join(member);
Post newPost = boardService.posting(member.getId(), member.getPosition(), "NEW POST", "hi");
System.out.println("post = " + newPost);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 게시판 서비스 객체 BoardServiceImpl
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final BoardPolicy boardPolicy = new ReadOnly();
@Override
public Post posting(Long id, Position position, String title, String content) {
Member member = memberRepository.findById(id);
boolean authority = boardPolicy.returnAuthority(member);
if(authority) {
return new Post(id, position, title, content);
} else {
// 권한이 없는 경우, 제목과 내용은 unavailable로 고정
return new Post(id, position, "unavailable", "unavailable");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 게시판 접근 권한 객체 ReadOnly
public class ReadOnly implements BoardPolicy {
@Override
public boolean returnAuthority(Member member) {
if(member.getPosition() == STUDENT){
// 학생인 경우 권한 없음
return false;
} else {
// 교사인 경우 권한 있음
return true;
}
}
}
■ 설계의 문제점
BoardServiceImpl 객체가 MemoryMemberRepository, ReadOnly 객체에 의존하고 있음
즉, 구현객체가 인터페이스뿐만 아니라 구현객체에도 의존하고 있음 ➜ DIP 위반
읽기만 가능한 권한인 ReadOnly 에서 ReadWrite로 바꾸려면, 클라이언트인 BoardServiceImpl을 변경해야함 ➜ OCP 위반
✏️ 문제점 해결
앞선 두 설계 과정에서 공통적으로 OCP, DIP를 위반하는 문제점이 발생했다.
어떻게 해결할 수 있을까? ➜ 클라이언트에서는 인터페이스만 결정하고, 사용할 구현 객체는 외부에서 주입받는다.
■ AppConfig의 등장
AppConfig는 구현객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스이다.
1
2
3
4
5
6
7
8
9
10
11
12
// MemberServiceImpl 수정
public class MemberServiceImpl implements MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository;
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// MemberServiceImpl이 어떤 저장소를 사용할지는 AppConfig에서 주입받음
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BoardServiceImpl 수정
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
// BoardServiceImpl이 어떤 정책을 적용할지는 AppConfig에서 주입받음
...
}
1
2
3
4
5
6
7
8
9
10
11
12
// AppConfig 클래스 생성
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
// memberService() 호출 시, 사용할 저장소를 결정해서 MemberServiceImpl에 주입한 뒤에 MemberServiceImpl 객체 리턴
}
public BoardService boardService(){
return new BoardServiceImpl(new MemoryMemberRepository(), new ReadOnly());
// BoardService() 호출 시, 사용할 저장소, 정책을 결정해서 BoardServiceImpl에 주입한 뒤에 BoardServiceImpl 객체 리턴
}
}
이제, MemberServiceImpl과 BoardServiceImpl은 인터페이스만 의존한다. 구현 객체는 생성자로 주입받는다. ➜ DIP 문제 해결
저장소를 바꾸거나, 권한 정책을 바꾸고 싶다면 AppConfig 클래스만 수정하면된다. ➜ OCP 문제 해결
✏️ AppConfig 리팩터링
현재 AppConfig에는 중복이 존재하고, 역할에 따른 구현이 눈에 잘 들어오지 않는다. 따라서, refactoring이 필요하다.
1
2
3
4
5
6
7
8
9
10
11
public class AppConfig {
/* 이전코드
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public BoardService boardService(){
return new BoardServiceImpl(new MemoryMemberRepository(), new ReadOnly());
}
new MemoryMemberRepository()가 중복, new ReadOnly()는 역할이 눈에 잘 들어오지 않음
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 변경 후
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
// memberRepository() 호출 시, 현재 설계에서 사용하고자 하는 저장소 반환
}
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
// memberService() 호출 시, 현재 설계에서 사용하고자 하는 서비스 객체 반환
}
public BoardPolicy boardPolicy(){
return new ReadOnly();
}
public BoardService boardService(){
return new BoardServiceImpl(memberRepository(), boardPolicy());
}
}
✏️ 수정사항 발생
이제, 설계 당시에 미확정 상태였던 요구사항들이 확정되었다고 가정해보자. 학생 회원은 글을 쓸 수 없도록 설계 했지만, 요구사항이 변경되어 글쓰기가 가능하게 바꾸고 싶다.
클라이언트인 BoardServiceImpl을 수정하지 않고, AppConfig만 변경하면 된다. ➜ 확장에는 열려있고, 변경에는 닫혀있다.
1
2
3
4
// AppConfig 내에서
public BoardPolicy boardPolicy(){
return new ReadWrite();
}
✏️ IOC, DI, 컨테이너
■ IOC
IOC(Inversion of Control): 제어의 역전
IOC란, 프로그램의 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것이다.
- AppConfig 사용 이전에는, 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.
- AppConfig 사용 후에는, 구현 객체는 자신의 로직만 실행하고 제어의 흐름은 AppConfig가 담당했다.
이러한 상황을 IOC라 부른다.
■ DI
DI(Dependency Injection): 의존관계 주입
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고, 클라이언트에 전달하여 클라이언트-서버간 의존관계가 연결되는 것을 의존관계 주입이라 한다.
- 실행 이전에, 클래스를 보고 의존관계가 판단되는 것은 정적 의존관계이다.
- DI를 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 사용하는 객체를 바꿀 수 있다.
■ 컨테이너
AppConfig 처럼 객체를 생성/관리하고 의존관계를 연결해 주는 것을 DI 컨테이너라 한다. (IOC 컨테이너라 부르기도 한다. 하지만, 의존관계 주입에 초점을 맞추어 DI 컨테이너로 자주 부른다.)
✏️ Spring으로 전환
위의 예제는 오로지 “순수한 자바”로만 작성되었다. AppConfig를 Spring의 기능을 사용해서 다음과 같이 작성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public BoardPolicy boardPolicy(){
return new ReadOnly();
}
@Bean
public BoardService boardService(){
return new BoardServiceImpl(memberRepository(), boardPolicy());
}
}
// annotation을 추가했다.
BoardApp 클래스 또한 스프링의 기능 중 하나인 ApplicationContext를 사용하도록 변경한다.
1
2
3
4
5
6
7
8
public class BoardApp{
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
BoardService boardService = applicationContext.getBean("boardService", BoardService.class);
...
}
ApplicationContext를 스프링 컨테이너라고 한다.
- 기존에는 AppConfig를 사용하여 DI를 했지만, 이제부터는 스프링 컨테이너를 활용한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
- 스프링 컨테이너는 @Bean이 붙은 메소드를 모두 호출해서 반환된 객체를 등록한다.
- 메소드 명을 스프링 빈의 이름으로 사용한다.
코드가 오히려 더 복잡해진 것 같은데… 스프링을 쓰는게 무엇이 좋은걸까??? 다음 포스트에서 알아보자.



