728x90

jsp 를 처음 경험해본다.

노드 js 로 되어 있었고 템플릿은 Html 이랑 비슷한 넌적스로 되어 있던 것을 jsp 로 바꾸는 과정에서 한글이 깨지는 현상을 만났다. 

이렇게 말이다..

 

해결

 

.jsp 파일 상단에

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

이 코드를 추가하니까 해결이 되었다.

728x90

몇일 코딩을 쉬면서 이론 공부를 하다가 일주일 뒤에 프로젝트를 열어서 코딩을 하려고 하니 이런 에러가 났다. 해결법은 다음과 같다.

 

나는 Gradle을 사용하고 있기 때문에, build.gradle 파일에 dependencies 항목을 다음과 같이 바꿔주었다.

implementation 'org.springframework.boot:spring-boot-starter-web'

 

 

참고

https://github.com/etture/dev_notes/issues/15

 

Spring Boot: error “package org.springframework.web.bind.annotation does not exist” · Issue #15 · etture/dev_notes

in build.gradle, change org.springframework.boot:spring-boot-starter to org.springframework.boot:spring-boot-starter-web

github.com

 

728x90

인턴을 하면서 새로운 것들을 많이 배웠다. 기술들을 마구잡이로 붙여서 그냥 기능만 찍어내는 위주로 개발을 해왔던 나는 이번 인턴을 통해 사수분으로부터 개발론에 대해 많은 것들을 느끼며 더 많은 준비가 필요함을 느끼며 다시 집구석으로 자진 하방 했다. 더 큰 도약을 위한 일보 후퇴라고 믿고 열심히 하고 있다. 유레카의 순간을 안겨주신 나의 사수였던 경석 씨 에게 경의를 표하며 이 글을 써내려 가려고 한다.

 

본론으로 들어가서, hexagonal architecture 기반으로 DDD, TDD, CDD 라는 내가 처음 접해본 방법론으로 개발을 진행했다. 모두 아시다시피

  • DDD - Domain Driven Design
  • TDD - Test Driven Design

이다. 그런데 CDD는 기존에 없던 용어로 알고 있다.

  • CDD - Commit Driven Design

커밋 주도 개발이라고 이름을 붙였다ㅋㅋ Jira에 Task를 만들고 해야 할 작업 내용에 대해 적고 브렌치를 파고 해야할 작업 내용을 계속 생각하면서 코딩을 하자는 취지이다. 그러니까 내가 무엇을 해야 하는지 알고 코드를 작성 하자는 의미였다. ( 항상 나는 A를 개발하면 B, C … 들도 생각이 나서 옆으로 세면서 코딩을 해왔는데 커밋 주도 개발을 하면서 또 다른 좋은 개발론에 대해 생각해보게 되는 계기가 되었다. )

 

이번 포스팅에서는 DDD만 써내려갈 계획이다.

DDD 를 하기 위해 이벤트 스토밍을 하기 위해서 Miro 를 이용했다.

 

본격적으로 내가 인턴을 하면서 DDD 를 설명하기에 앞서 이와 같은 조건들을 사용하여 설계해 나갔다.

  1. 액터(Actor) 는 커맨드(Command) 를 내린다. 예를 들어, “서비스 사용자는 push 알람 설정 변경을 한다.” 는 예시를 들 수 있다.
  2. 커맨드는 애그리거트 단위의 기능을 제공하기도 하고 **액터(이벤트, 유저 그리고 외부 시스템)**에 의해서 수행된다. 이벤트가 들어오면 커맨드(명령)가 실행되고, **유저(어플리케이션)**가 API 를 요청해서 실행되는 경우도 있고, 구글 로그인, 이메일 발송, 알람 발송(Twillio, FCM, OneSignal…) 등 외부 시스템에서 실행할 수 있다. 나는 다른 도메인에서 실행되는 외부 서비스는 해당 서비스를 담당하고 있는 도메인에서 이벤트로 표시하였다.
  3. 액터(이벤트, 유저, 외부시스템)로 부터 시작된 동작의 결과로 이벤트가 생성된다. 이벤트는 액터에 의해 발생한 일을 의미한다. 예를 들어, “push 알람 설정 변경됨" 이 사례를 들 수 있다.
  4. Event의 결과는 Read Model 에 영향을 준다. Read Model은 CQRS 에서 비롯된 개념이다. 말 그대로 읽기와 쓰기를 분리해서 읽기에 특화된 모델이다. 커맨드를 나와서 이벤트가 되면 queue 에 이벤트가 쌓여서 그것을 읽기만 하는 모델이다. 이런 방식은 성능에 장점이 있다. 그리고 단순성이다. 데이터베이스에서 데이터를 가져오는 더 간단한 방법은 없다.
  5. 정책은 “A 이벤트가 발생하면 B 커맨드를 트리거 한다" 를 표현한다. ( 예를 들어 )즉 커맨드는 사실 액터와 Policy에 의해서 트리거 될 수 있는 셈입니다.
  6. 별도로 의문점이나 추후 해결해야하는 티켓이 있다면 핫스폿 으로 표현할 수 있다.

 

이벤트 정의

이벤트들은 CQRS 원칙에 따라 Read 를 제외한 Create, Update, Delete 성질을 가진 이벤트들이 주로 이루고있다. 그 외 Read 는 Read Model 로 표현되는 경우가 많다. SRP를 생각하며 이벤트들을 쪼개는 작업은 정말 애매했다. 그럴 때 마다 잘게잘게 쪼개야 한다는 것을 터득했다. (이 방법이 주로 괜찮았음)

 

이벤트 표현 방법

보통 좌 에서 우로 표현하며, 좌에서는 이벤트를 발생시키기 위한 명령(커맨드)를 배치하고 우에서는 커맨드가 실행하고 발생되는 이벤트 내용을 적는다. 커맨드와 그에 따라 발생되는 이벤트는 주로 동사형태로 작성된다. 

위를 해석해보자면, “이벤트가 왔을 때 sms 메시지를 저장하고 저장되면 sms 전송을 한다.” 로 해석된다.

“완료”는 개발이 완료 되었을 때 표시하는 용도로 사용했다.

 

또 다른 케이스가 있다. 커맨더가 실행되고 난 후 여러가지 케이스의 이벤트가 발생할 수 있다.

위와 같은 상황에서 “push 메시지 저장” 이라는 커맨드가 발생하고 “push 전송 메시지 저장됨" 이랑 “push 비전송 메시지 저장됨" 이 있을 수 있다. 이 때 위와 같이 발생되는 이벤트를 위아래로 표현할 수 있다.

 

 

액터와 정책으로 연결된 어그리개잇들 그리고 그렇지 않은것

처음에 DDD를 접하고 설계를 할 때, 액터가 있는 티켓도 있고 없는 티켓도 있었는데 여기서 좀 헷갈렸다.

이 티켓들을 예시로 들어보면, 액터가 이벤트인것을 알 수 있는데, 액터의 성질인 이벤트가 들어오면 해당 “push 메시지 저장" 커맨드를 수행하고 “push 전송 메시지 저장됨" 이벤트가 발생하면 “push 알람 전송" 커맨드가 실행되는 것을 알 수 있다. 여기에서 또 확인할 수 있는 부분이 “push 알람 전송" 커맨더 앞에 액터가 없는 것을 확인할 수 있다. 이 의미는 외부적으로 API 를 열지 않고 마스터 서버 내부적인 API 라는 뜻이다. 즉 액터가 없으면 외부적으로 API 를 열어놓지 않았다는 의미가 된다.

 

반면 이것은 정책으로 연결되어 있지 않은 티켓이다. 토큰을 등록하는 기능을 하는데 외부로 부터 API 를 열어둔 것을 확인할 수 있다.

 

 

 

 

 

 

 

또 다른 케이스는 이렇게 두 가지의 액터를 가질 수 있다. 해석하자면, 이벤트와 사용자 둘 다 “이메일 메시지 저장"커맨드를 실행하고 “이메일 메시지 저장됨" 이벤트를 발생할 수 있다.

 

지금까지 여러 경우의 이벤트 스토밍 된 티켓들을 봤다. 잘 생각해보면 정책으로 연결되어있는 것이 없을 수록 좋다. 그래야 내부적으로 복잡하지 않기 때문에 서버에 부담을 줄일 수 있기 때문이다. 잘 생각하고 판단해서 티켓들을 만들어야 했다.

 

 

 

Read Model 로 Read 작업 분리 이유

API 로 커맨드가 실행되고 커맨드를 받은 도메인에서 이벤트가 발생하고 queue 에 들어간다. QueryModel 은 queue 에 있는 이벤트들을 끌어와서 처리하는 방식이다. 도메인별로 서버가 각각 따로되어 있는데 비즈니스 로직을 맡고있는 도메인들이 죽어도 앱이 돌아가는 이유는 QueryModel 에서 화면을 보여주고 있기 때문이다. 기능은 수행하지 못할지라도 완전히 앱이 죽는 것 보다 앱을 켜서 사용은 할 수 있는 것이 훨씬 사용자 경험상으로 좋기 때문이다.

 

이 그림에서는 QueryModel 이라고 했는데 ReadModel 이랑 다를바 없다고 보면 된다.

 

TDD

테스트 주도 개발이다. 먼저 테스트 코드를 작성하고 로직을 짜는 식으로 코딩을 해야 진정한 TDD 라고 배웠는데 해보니까 쉽지 않았다. 회사에 들어와 처음 해보는 테스트 코드 작성이기도 한지 생각의 연속이었다. 빨리 서비스를 출시해야하기 때문에 많은 시간이 걸릴 것 같아서 결국에는 로직을 먼저 코딩하고 테스트를 짜는 식으로 하였다. 먼저 테스트 코드를 짜는 방식으로 연습이 필요하다고 느꼈다.

테스트 코드는 각 애그리거트 당 나오는 레파지토리 테스트를 하고 커맨드핸들러 들을 테스트 했다.

 

헷갈렸던 것들

테스트로 푸시메시지를 보내기 위해서 가짜 객체를 DI 해줘야 하는데 Stub 이랑 Spy 가 헷갈렸다.

 

- Stub: 로직에 영향을 끼친다. (Repository 같은 경우는 findbyId 같은것들도 있기에 stub이고, template 같은 것들도 stub이다..)

- Spy: 로직에 영향을 끼치지 않는다. (Send 같은 것들은 send 테스트 해보고 싶은거니까 spy 이다.)

 

이렇게 정리가 되었다.

 

CDD

앞서 말했듯이 커밋주도개발은 어떤것을 개발해야하는지 먼저 생각한 후 지라에 적고, 해당 브렌치를 파고 해당 브렌치에서는 지라에 적은 것들만 개발하자는 취지였다. 다시말해 관심사를 정해놓고 개발하자는 것이다. 예를 들어 

위와 같은 방식을 ⬇️ 이렇게 지라에 스토리로 하든 작업으로 하든 등록을 해 놓고 코딩을 시작하는 방식으로 하였다.

 

hexagonal architecture

육각형 아키텍처는 포트와 어댑터을 가장자리에 두고 비즈니스 로직은 어디에도 의존되지 않아야하는 것이 아이디어이다. 프로젝트의 핵심인 비즈니스 로직을 보호하고 외부 툴 및 기술로 부터 이를 격리하기 위해서 추상화를 하였다. 수시로 변경되는 요구 사항으로 인해 코드에 큰 변경이 필요한 소프트웨어가 아니라고 생각했기 때문에 지속 성장이 가능한 구조라고 생각이 되었다. 추상화는 어디 까지 해야하는지에 대한 질문을 사수분이 던져주셨다. 정말 애매했다. 결론은 Infrastructure 는 추상화 하는 것이 좋고 domain 은 추상화 하기가 어렵다로 났다. 여기에 대한 나의 생각을 덧붙이고 싶지만 육각형 아키텍처를 잘 모르는 상태에서 말하기에는 너무 주제넘는다고 생각한다. 추후 이에 대해 공부를 하면서 더 생각해볼것이다. 이에 대한 더 자세한 내용은 따로 포스팅 할 예정이다.

 

마치며

말로만 들었던 DDD 를 완전히 처음 접해보았다. 이번을 계기로 개발에 대한 나의 인식이 많이 바뀌게 되는 터닝포인트가 되었다. 어릴 때 정말 재미있게 한 레고가 떠올랐다. 처음 레고를 사면 함께 들어있는 설명서도 없이 블록들만 있는 상태에서 설명서를 내가 만들고 그 설명서를 보면서 블록을 가지고 만드는 것 같았다. 처음 배운다고 정말 힘들었지만 조금이라도 알고나니 정말 재미있다. 이런것들을 접하게 해준 나의 사수에게 정말 감사하다.

728x90

대부분의 의존관계는 애플리케이션이 끝날 때 까지 변하면 안된다. 불변함을 유지해야 한다. 이를 생성자 주입이 한다. 수정자 주입을 사용하면 setXxx 메서드를 public 으로 열어두어야 하므로 누군가가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다. 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

 

만약에 수정자 의존관계를 쓴다고 해보자.

결과는 

NPE 이 난다. 실제로 OrderServiceImpl 에 어떤 의존관계 주입이 되어야 하는지 한눈에 알지 못하고 코드를 까봐야 알 수 있다. 그러나 생성자 주입을 사용하면 

 

이렇게 바로 문제점이 표시되면서 실행 시켜보면 컴파일 오류뜬다. 생성자가 지정하면 필수값이 들어가야하기 때문에 바로 필요한 값을 인지할 수 있다. 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류이다.

 

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

참고: 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.

 

정리

  • 생성자 주입 방식을 선택하는 이유는 여러가지가 ㅇ지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 항상 생성자 주입을 선택해라. 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다.
728x90

의존관계 주입은 크게 4가지 방법이 있다.

1. 생성자 주입

2. 수정자 주입(setter 주입)

3. 필드 주입

4. 일반 메서드 주입

 

1. 생성자 주입

생성자에 @Autowired 가 된 것을 볼 수 있다. 첫번째로 스프링 컨테이너가 컴포넌트 스캔을 하고 OrderServiceImpl 이 @Component 가 붙어있어서 스프링 빈에 등록이 될 때, 등록이 되면 생성자가 호출 되어야 하니까, 생성자를 호출할 때 @Autowired 가 있는지 찾는다. 있으면 생성자가 요구하는 파라메터들의 타입들(여기서는 MemberRepository, DiscountPolicy) 를 가지고 있는 스프링 빈을 스프링 컨테이너가 꺼내서 주입을 시켜준다. 

 

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
  • 지금까지 우리가 진행했던 방법이 바로 생성자 주입이다.
  • 특징
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변, 필수 의존관계에 사용

여기서 불변은 누군가가 수정하는 메서드를 만들지 않는 이상 딱 한번만 호출되기 때문에 변하지 않는다. 개발에서 불변이라는 것이 정말 중요하다. 개발 습관에서 한계점, 제약이 있어야 한다. 다 퍼블릭으로 열어두면 뭐가 수정이 되었는지 어디서 수정되었는지 알 수 없어진다. 생성자를 통해서만 의존관계가 주입이 되고 어디서도 생성자에서 설정 된 인스턴스들이 수정되는 메서드가 있어서는 안된다. 왜냐하면 개발자의 의도는 처음에 AppConfig 로 스프링 컨테이너에 빌딩되어서 올라갈 때 연관관계를 다 만들고 더이상의 수정 없이 끝내고 싶어 한것이다. 예를 들어, 공연 도중에 배우들을 바꿀 일이 없고, 공연이 시작하기 전에 배우들을 다 정해놓고 끝내고 싶은 것이다. 결론적으로 값을 세팅하고 더이상 값을 바꾸면 안될때, 가급적으로 생성자 에다가 값을 넣고 수정자 메서드(setter 메서드) 를 만들지 않으면 된다. 이렇게 하면 버그도 줄일 수 있다. 

여기서 필수는 필드에 변수들이 final 로 정의를 해야한다는 의미이다. final 로 변수를 정의 하면 꼭 그 변수에 할당되는 값이 있어야 한다. 값이 무조건 있어야 할 때를 가리킨다.(특히 생성자에 들어가는 값들) 

 

중요: 생성자가 하나만 있으면 @Autowired 가 안붙어 있어도 인식을 해서 의존관계 주입이 일어난다.

 

2. 수정자 주입

  • setter 라 불리는 필드의 값을 변경하는 수정자(setter) 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 특징
    • 선택, 변경 가능성이 있는 의존관계에 사용
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

이렇게 각 타입별로 setter 들을 만들고 각각에 @Autowired 를 붙여도 의존관계 주입이 된다. 그러나 @Autowired가 없으면 안된다. 

 

여기서 스프링 컨테이너의 라이프사이클을 말해보면, OrderServiceImpl 을 스프링 컨테이너에 등록을 한다. 그 다음에 두 가지로 나뉘는데 첫 번째는 스프링 빈을 등록하고, 의존관계를 자동으로 주입한다.( @Autowired 가 걸린 애들을 연관관계를 주입시킨다 ) 해당 방법(setter 주입)은 스프링 빈을 등록하고 의존관계 주입 준비 단계 에서 일어날 때 일어난다. 생성자 주입은 하나의 인스턴스가 스프링 빈에 등록이 되면서 생성자로 바로 다른 것들도 스프링 빈이 등록되기 때문에 선택적으로 주입하는게 아니라 필수적으로 주입이 된다.

 

참고: @Autowired 의 기본 동작은 주입할 대상이없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다

생성자 주입이랑 수정자(setter)주입 이 함께 쓰이면 어떻게 될까??

생성자로 부터 인스턴스가 주입이 되고 그 다음 순서 상관없이 수정자 주입으로 인스턴스가 주입된다. 몰론 여기서도 스프링 컨테이너는 싱글톤을 보장해주기 때문에 생성자로 주입된 인스턴스들과 setter 로 주입된 인스턴스들은 모두 같은 객체들이다.

 

3. 필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법이다.
  • 특징
    • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용하지 말자!
      • 애플리케이션의 실제 코드와 관계 없는 테스트 코드 에서는 괜찮다.(스프링 강좌 24분에 예시 나옴)
      • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

 

이렇게 하면 문제점이 setter 가 필요하다. 필드 주입을 사용할 바에는 setter 주입을 사용하는 것이 나은 형상이다.

 

4. 일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 특징
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용하지 않는다.

 

그냥 메서드 위에 @Autowired 있고 이 메서드를 통해 의존관계 주입을 해준다. 사실상 수정자 주입이랑 같다. 이것을 생성자 주입이랑 수정자 주입에서 다 해결하기 때문에 이것을 사용할 일은 거의 없다.

 

참고: 어쩌면 당연한 이야기이지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지않는다.
728x90

컴포넌트 스캔과 의존관계 자동 주입 하기를 배워볼 것이다. 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean 이나 XML 의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다. 예제에서는 몇개가 안되었지만 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면서 일일이 등록하기도 귀찮고 설정 정보도 커지고, 누락하는 문제도 발생한다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다. 

 

코드로 컴포넌트 스캔과 의존관계 자동 주입을 알아보자.

 

먼저 기존 AppConfig.java 는 과거 코드와 테스트를 유지하기 위해 남겨두고, 새로운 AutoAppConfig.java 를 만들자.

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다.

기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다!

참고: 컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다. 그래서 excludeFilters 를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외했다. 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다.

컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.

참고: @Configuration 이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.

이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주자.

 

MemoryMemberRepository @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository {}

RateDiscountPolicy @Component 추가

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

OrderServiceImpl @Component, @Autowired 추가

@Component
  public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;
      private final DiscountPolicy discountPolicy;
@Autowired
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}

 

@Autowired 를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

 

AutoAppConfigTest.java

  package hello.core.scan;
  import hello.core.AutoAppConfig;
  import hello.core.member.MemberService;
  import org.junit.jupiter.api.Test;
  import org.springframework.context.ApplicationContext;
  import
  org.springframework.context.annotation.AnnotationConfigApplicationContext;
  import static org.assertj.core.api.Assertions.*;
  public class AutoAppConfigTest {
      @Test
      void basicScan() {
          ApplicationContext ac = new
  AnnotationConfigApplicationContext(AutoAppConfig.class);
          MemberService memberService = ac.getBean(MemberService.class);
          assertThat(memberService).isInstanceOf(MemberService.class);
      }
}

 

AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일하다. 설정 정보로 AutoAppConfig 클래스를 넘겨준다.실행해보면 기존과 같이 잘 동작하는 것을 확인할 수 있다.

 

컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자.

1. @ComponentScan

@ComponentScan @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.

빈 이름 기본 전략: MemberServiceImpl 클래스 ➡️ memberServiceImpl

빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component("memberService2") 이런식으로 이름을 부여하면 된다.

 

2. @Autowired 의존관계 자동 주입

생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.

getBean(MemberRepository.class) 와 동일하다고 이해하면 된다. 더 자세한 내용은 뒤에서 설명한다.

생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.

728x90

@Configuration 을 알아보기 위해서 이 코드를 살펴보자,

시나리오

1. @Bean memberService 를 호출하면 ➡️ MemoryMemberRepository() 를 한번 호출한다. 

2. @Bean orderService 를 호출하면 ➡️ MemoryMemberRepository() , discountPolicy() 를 호출한다.

 

이렇게 하면 결과적으로 각각 다른 2개의 MemoryMemberRepository() 가 두 번 호출 되니까 싱글톤이 깨지는게 아닐까요?? 스프링 컨테이너가 싱글톤을 보장해준다고 했는데 과연 그럴까?? 직접 테스트 해보자

 

✅ 테스트 방법

1. MemoryMemberRepository 를 불러오는 MemberServiceImpl 에서 

테스트 용도로 MemberServiceImpl, OrderServiceImpl 둘 다 각각 생성자로 들어오는 memberRepository 의 값을 확인하는 getMemberRepository 메서드를 만들었다.

 

2. 각각의 memberRepository 를 테스트 코드를 통해 확인해보자.

밑에는 테스트코드이고

밑에는 테스트 코드의 결과이다.

결과는 세개의 memberRepository가 같은 것을 확인할 수 있다. 분명 세번의 다른 memberRepository의 인스턴스가 호출될 것 같았지만 모두 같은 인스턴스가 공유되어 사용되고 있음을 확인할 수 있었다. 어떻게 된 일일까? 혹시 두 번 호출이 안되는 것일까? 다음 실험을 통해 알아보자. 

 

이렇게 호출 되는 부분마다 찍어보면서 확인해보았다. 

찍어본 세 개의 메서드 각각 한번씩 호출 되었는 것을 볼 수 있다. 이 비밀이 어디에 있을까?? ➡️ @Configuration 을 적용한 AppConfig 에 있다.

 

시나리오

1. AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해보자.

순수한 클래스라면 다음과 같이 출력되어야 한다.

class hello.core.AppConfig

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB 이라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다. (실제로는 CGLIB의 내부 기술은 매우 복잡하다.)

 

  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • 덕분에 싱글톤이 보장되는 것이다.

 

만약 @Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?

 

@Configuration 을 주석처리 하고 @Bean 만 적용해보고 밑에 테스트 코드를 다시 돌려보았다.

결과로 이렇게 나왔다. 

 

CGLIB 기술을 사용 하지 않은 순수한 AppConfig 가 나왔다. 그리고

memberRepository가 여러번 호출되는 것을 확인할 수 있듯이 순수한 자바 코드가 나오면서 싱글톤이 깨진다!!

 

 

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
    • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.

 

728x90

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

그러면 무상태(stateless)로 설계해야 한다!

무상태란.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

 

상태를 유지할 경우 발생하는 문제점 예시

최대한 단순히 설명하기 위해, 실제 쓰레드는 사용하지 않았다.

 

  • ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.
  • StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
  • 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
  • 실무에서 이런 경우를 종종 보인다고 하는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다고 한다.(몇년에 한번씩 꼭 만난다고 한다.)
  • 진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.

 

728x90

싱글톤 컨테이너

= 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성) 으로 관리한다. 
= 스프링 빈(Bean)이 바로 싱글톤으로 관리되는 빈이다.

 

1. 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.

  • 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.

  • 위 그림에서 보면 빈 객체를 미리 스프링 컨테이너(싱글톤 컨테이너)가 등록해서 관리해준다. 그리고 조회하면 관리되고 있는 객체를 불러온다. 여러개 조회해도 이미 관리되고 있는 객체를 조회한다. 예를 들어 memberService 를 몇번을 불러도 MemberServiceImpl@x01 를 조회해준다.

2. 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.

3. 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • 싱글톤 패턴을 위해서 들어가는 지저분한 코드들을 안들어가도 된다. 그리고 " (구체 클래스).getInstance() " 이렇게 해야하기 때문에 DIP 에 위반한다. 스프링이 관리 해주기 때문에 이런 코드가 다 빠져도 된다.
  • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다. ➡️ 스프링이 다 관리 해주는 덕분이다.

 

이 테스트로 같은 객체를 불러오는 것을 확인할 수 있다.

조회할 때 마다 스프링이 처음에 빈 등록한 것을 계속 반환해주는 것을 확인할 수 있다. ➡️ 싱글톤 맞다~

위 테스트에서 MemberServiceImpl 객체를 불렀는데 이 객체의 코드를 확인해보자. 과연 싱글톤 관련된 코드가 있을까? ⬇️⬇️⬇️

 

싱글톤 관련된 코드는 없는 것을 확인할 수 있다.

 

 

싱글톤 적용 Before After

  • Before

 

  • After

스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 자세한 내용은 뒤에 빈 스코프에서 설명하겠다.

 

이러한 싱글톤 방식에도 주의점이 있다.(매우 중요) 다음 글에서 확인해보자.

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

 

[Spring] 싱글톤 패턴 주의점

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글

ydontustudy.tistory.com

 

728x90

클라이언트가 요청할 때 마다 새로운 인스턴스가 생긴다. 이것은 서버에 많은 비용을 요구한다. 정말로 클라이언트의 요청마다 새로운 인스턴스가 생성이 되는지 확인을 해보겠다.

 

참조값을 확인하면 서로 다른 인스턴스인 것을 알 수 있다.

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다. -➡️ 메모리 낭비가 심하다.
  • 💡해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. ➡️ 싱글톤 패턴

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

 

  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

  1. private으로 new 키워드를 막아두었다.
  2. 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.

Singleton 을 적용한 후 이제는 새로운 인스턴스를 만들지 않는다는 것을 확인할 수 있다.

 

💡참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

 

싱글톤 패턴 문제점

싱글톤은 생성된 인스턴스를 공유를 할 수 있고 확실한 객체 하나가 있다는 것이 보장이 되지만 수많은 단점이 있다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. ➡️ DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다. (싱글톤은 지정해서 가져오고 미리 인스턴스를 받아놓아서 설정이 끝나버리기 때문에 유연한 테스트가 어렵다.)
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. (DI 를 적용하기 어렵다.)
  • 안티패턴으로 불리기도 한다.

이러한 단점을 스프링 프레임워크가 전부 다 해결해준다. ➡️ 객체를 싱글톤으로 관리해준다. 

 

다음은 스프링 컨테이너(싱글톤 컨테이너) 의 역할에 대해서 알아보자.

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
 

[Spring] 싱글톤 컨테이너

싱글톤 컨테이너 = 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성) 으로 관리한다. = 스프링 빈(Bean)이 바로 싱글톤으로 관리되는 빈이다. 1. 스프링

ydontustudy.tistory.com

 

 

+ Recent posts