[스프링 핵심 원리] 스프링의 핵심 원리

JUJU·2024년 2월 11일
1

Spring

목록 보기
3/21
본 포스트는 김영한 개발자님의 스프링 핵심 원리 강의를 듣고 정리한 것입니다.
※ 코드는 강의에서 사용된 것과 다릅니다.
jaewon-ju Github Address

실제 프로젝트를 설계해보면서 왜 스프링이 필요한지, 스프링의 핵심 원리는 무엇인지 이해해보자.

✏️ 들어가기 전에..

JUnit이란, 테스트를 위한 자바용 프레임워크이다.

비즈니스 로직을 구현한 뒤에, JUnit 프레임워크를 사용하여 반드시 테스트를 해봐야 한다.

  • @Test 어노테이션을 통해 테스트 메소드임을 선언할 수 있다.
  • @BeforeEach 어노테이션을 통해 각 테스트 실행 전에 실행될 메소드를 선언할 수 있다.
  • Assertions.assertThat(A).isEqualTo(B) 를 통해 A와 B가 같은지 확인하고 test 성공 여부를 알 수 있다.

✏️ 비즈니스 요구사항

학원용 게시판 프로그램을 순수한 자바 코드로 설계한다.

■ 회원

  • 회원을 가입하고 조회할 수 있다.
  • 회원은 학생 회원과, 교사 회원이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

■ 게시판 이용

  • 회원은 게시판을 이용할 수 있다.
  • 회원 자격에 따라 게시판에 글을 쓸 수 있다.
  • 학생 회원은 글을 읽는 기능만 가능하게 설계하고 싶다.
  • 하지만, 학생 회원의 게시판 이용 범위를 확실하게 결정하지 못했다. (미확정)

요구사항을 보면 회원 데이터, 이용 범위 등은 지금 결정하기 어렵다. 이러한 세부사항이 결정될 때까지 개발을 미룰 수는 없다! ➜ 다형성을 이용한다.




✏️ 회원 도메인 설계

회원 클래스 다이어그램

회원 객체 다이어그램

// Member는 id, name, position 필드와 Getter/Setter를 가지는 클래스
// Position은 열거형으로 STUDENT, TEACHER 2가지 종류가 있다.
Member 클래스 구현 생략
// MemberRepository 인터페이스는 save, findById를 추상메소드로 갖고있다.
// MemoryMemberRepository 구현 객체는 save, findById 메소드를 오버라이딩했다.
MemoryRepository 인터페이스, MemoryMemberRepository 클래스 구현 생략
//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 위반



✏️ 게시판 도메인 설계

클래스 다이어그램

객체 다이어그램

게시판에서 게시글을 작성하는 과정

순서실행 주체기능
1BoardAppmember.join() 으로 회원 가입 -> 학생 회원이라 가정
2BoardAppboardService.posting() 호출
3BoardServiceImpl저장소에서 해당 member 정보 가져옴
4BoardServiceImplboardPolicy.returnAuthority() 호출
5ReadOnly학생 회원이므로 false 반환
6BoardServiceImplauthority가 false이므로 제목과 내용 인수는 unavailable로 설정
6BoardServiceImplPost 타입으로 게시글 반환
7BoardApp2번의 호출로 받은 게시글 출력
// 게시판 도메인 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);
    }
}
// 게시판 서비스 객체 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");
        }

    }
}
// 게시판 접근 권한 객체 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는 구현객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스이다.

// 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에서 주입받음
    ...
 }
// 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에서 주입받음
    	...
 } 
// 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이 필요하다.

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()는 역할이 눈에 잘 들어오지 않음
	*/
// 변경 후

   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만 변경하면 된다.
➜ 확장에는 열려있고, 변경에는 닫혀있다.

// 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의 기능을 사용해서 다음과 같이 작성할 수 있다.

@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를 사용하도록 변경한다.

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이 붙은 메소드를 모두 호출해서 반환된 객체를 등록한다.
  • 메소드 명을 스프링 빈의 이름으로 사용한다.

코드가 오히려 더 복잡해진 것 같은데... 스프링을 쓰는게 무엇이 좋은걸까???
다음 포스트에서 알아보자.


REFERENCE

스프링 핵심 원리 - 김영한 개발자님

profile
개발자 지망생

0개의 댓글

관련 채용 정보