스프링 핵심원리 - 기본편 : 싱글톤 컨테이너
웹 애플리케이션과 싱글톤
스프링을 사용하지 않은 순수한 DI컨테이너 AppConfig를 만들었다.
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 호출할때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
//참조값이 다른 것을 확인
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
meberService()가 호출될때마다 새로운 객체를 생성하므로 meberservice1 과 meberService2는 공유되지않는 다른 인스턴스이다.
싱글톤 컨테이너
순수한 자바의 컨테이너와는 다르게 스프링컨테이너는 싱글톤으로 Bean 객체를 생성하고 등록한다. 스프링은 기존 싱글톤이가지는 문제점을 해결하고 장점만을 가진 싱글톤 객체를 생성하고 관리한다.
- 기존 싱글톤의 문제점
- 싱글톤 패턴을 구현하는 코드가 많이 들어감
- 구현객체를 클라이언트에서 직접 사용(의존)하여 DIP 위반, OCP 위반 가능성 높음
- 테스트 힘듬
- 자식 클래스 만들기 힘듬
스프링이 싱글톤으로 인스턴스를 관리하기 때문에 개발자는 싱클톤 패턴으로 클래스를 구현하지 않는다. 때문에 싱글톤 패턴 구현으로 오는 문제점들이 발생하지 않는다. 사용할 때 직접적으로 구현객체를 사용하는 것이아니라 의존관계 주입을 통해 사용되기 때문에 스프링이 하나의 인스턴스를 유지시켜주면서 오는 싱글톤의 장점만을 얻을 수 있는것이다. 아래 코드는 스프링 컨테이너에 등록된 meberService 빈이 하나의 객체만 생성되기 떄문에 조회한 meberService1 과 meberService2 의 참조값이 같음을 보여주는 테스트코드다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//참조값이 같음을 확인
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
싱글톤 방식의 주의점
상태를 가지면 안된다. -> 특정 클라이언트가 값을 변경할 수 있는 필드를 가지면 안된다.
싱글톤은 하나의 인스턴스를 가지기 때문에 클라이언트가 필드의 값을 변경하게 되면 다른 클라이언트는 변경된 필드값을 조회하게 된다. 다른 클라이언트들 끼리의 필드 값 변경은 각각의 클라이언트들에게 서로 예측 불가능하다. 아래는 상태를가지는 StatefulService 클래스이다.
public class StatefulService {
private int price;
public void order(String name,int price){
System.out.println("name = " + name + ", price = " + price);
this.price = price; //문제 발생
}
public int getPrice() {
return price;
}
}
StatefulService 클래스는 order() 메소드를 통해 변경가능한 멤버변수 price를 가지고 있다. 이는 값을 변경할 수 있는 필드이다. 아래 코드에서 이로인한 문제점을 확인할 수 있다.
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//Tread A: 사용자A 10000원 주문
statefulService1.order("userA", 10000);
//Tread B: 사용자B 20000원 주문
statefulService2.order("userB", 20000);
//Tread A: 사용자A가 주문 가격 조회
int price = statefulService1.getPrice();
Assertions.assertThat(price).isNotEqualTo(10000);
}
@Configuration
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
order() 메소드는 멀티스레드 환경에서 호출되었다고 가정한다. 코드처럼 statefulService1 과 statefulService2는 각각 다른 지역변수에 StatefulService 객체를 담고있지만 Spring 컨테이너를 통해 같은 빈 객체를 참조한다. 떄문에 userA의 order() 호출에는 StatefulService의 price가 10000, userB의 호출에는 price가 20000으로 변한다.
그 후 getPrice() 메소드를 userA 가 호출하면, 10000이 반환될 것이라고 기대했던 것에 반해 20000을 반환 받는다. userB에 의해 필드가 변경되었기 때문이다. 클라이언트가 변경 가능한 필드를 가지게 되면 이와같은 문제들이 발생할 수 있다.
@Configuration과 싱글톤과 바이트코드 조작 마법
Spring@Configuration 바이트코드 조작을 통해 스프링 빈이 싱글톤이 되도록 보장해준다. 아래는 AppConfig 클래스의 코드이다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
메소드들을 보면 memberService(), orderService() 에서 각각 memberRepository()를 호출한다. 만약 이코드가 그대로 실행되면 인스턴스는 하나로 유지되지 못하고 여러개의 빈이 등록되게 될 것이다. @Configuration은 인스턴스가 하나만 생성될 수 있도록 바이트코드 조작을 통해 새로운 클래스를 만든다. 이클래스는 내가 짠 클래스에서 만약 이미 생성된 동일한 클래스의 빈이 있다면 새로운 빈을 생성하지 않도록하는 코드를 가진 새로운 AppConfig 클래스를 만들고 빈에 등록한다. 즉, 빈에 등록되는 AppConfig는 순수하게 내가 짠 코드가 아니라 스프링에 의해 조작되어 새로 만들어진 클래스이다. 이는 아래 코드를 통해서 확인할 수 있다.
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//멤버서비스의 클래스
Assertions.assertThat(MemberServiceImpl.class).isEqualTo(ac.getBean(MemberService.class).getClass());
//AppConfig 의 클래스
Assertions.assertThat(AppConfig.class).isNotEqualTo(ac.getBean(AppConfig.class).getClass());
}
위의 테스트 코드처럼 AppConfig아닌 다른 빈의 클래스는 구현클래스와 같다. 하지만 AppConfig는 내가 구현한 클래스와 빈에 등록된 빈의 클래스와 다르다. 이는 스프링이 싱글톤을 가능케하는 AppConfig 클래스를 새로만들어 빈에 등록했기 때문이다.
그 외 : AssertJ에서 isEqualTo 와 isSameAs 차이
AssertJ에 isEqualTo 와 isSameAs 의 차이를 조사했다.
isSameAs : == 을 통해 참조값 조사
isEqualTo : 객체의 값이 같은지 조사
이를 아래와 같은 테스트코드를 작성해 테스트 해보았다.
@Test
@DisplayName("isEqualTo vs isSameAs")
void compareAssertMethod(){
String str1 = "abc";
String str2 = new String("abc");
Assertions.assertThat(str1).isEqualTo(str2);
Assertions.assertThat(str1).isNotSameAs(str2);
}
str1 과 str2 는 둘다 "abc" 라는 같은 값을 가지지만 str2는 생성자를 통해 생성되어 str1가 별개의 메모리 영역에 존재하는 다른 객체이다. 때문에 isEqualTo() 로 이 둘을 비교하면 통과하지만 둘의 참조값은 다름으로 isNotSameAs()를 사용해야지 통과된다.(isSameAs는 통과X)
프로그래머스 코딩테스트 연습문제 - 최대공약수와 최소공약수
최대공약수를 구하는 재귀코드와 이를통해 최소공약수를 구하는 공식을 알고있으면 아주 빠르게 풀 수 있는 문제였다. 직접 손으로 적어가면서 흐름을 적어보긴 했는데 이정도는 그냥외우는게 좋을 것 같다 코드는 다음과같다.
class Solution {
public int[] solution(int n, int m) {
int gcd = gcd(n, m);
return new int[]{gcd, n * m / gcd};
}
private int gcd(int a, int b){
if (b == 0) {
return a;
}
else{
return gcd(b, a % b);
}
}
}
최대공약수를 반환하는 메소드 gcd는 재귀함수로 구현되어 있다. a 와 b의 대소는 상관없다. b로 들어가는 매개변수는 다음번엔 a로 들어간다. 최소공배수는 원래 최초 두 수의 곱 나누기 최대공약수이다.
결론 및 느낀 점
스프링에등록되는 빈의 주소값들을 직접 찍어보면서 어떻게 Spring이 빈을 관리하는지 이해할 수 있게되어 앞으로 어떻게 코드를 짜야하는지에 대한 방향성을 조금더 잡을 수 있었던거 같다. 이제 조만간 개인프로젝트를 시작하여 이렇게 공부한 내용들을 바탕으로 코드를 작성하고, 계속 공부하면서 리팩토링 해 나갈 예정인데 재밌을 것 같다 얼른 하고싶다!!
'TIL' 카테고리의 다른 글
[2022-1-14]스프링 공부 + 프로젝트 준비 (0) | 2022.01.15 |
---|---|
[2022-1-13]스프링 핵심 원리 - 기본편 : 컴포넌트 스캔 (0) | 2022.01.14 |
[2022-1-10]스프링 핵심원리 - 기본편 : 스프링 컨테이너와 스프링 빈 (2/2) / 프로그래머스 코딩테스트 연습문제 - 실패율 (0) | 2022.01.10 |
[2022-1-9]프로그래머스 레벨1 10문제 (0) | 2022.01.10 |
[2022-1-8] 스프링 핵심 원리 - 기본편 : 스프링 컨테이너와 스프링 빈 (1/2) / 프로그래머스 연습문제 - 폰켓몬(Set, HashSet) (0) | 2022.01.09 |