일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 레디스 동시성
- 검색어 추천
- 백준
- docker
- gRPC
- branch 전략
- 구현
- spring event
- 디버깅
- 좋은 코드 나쁜 코드
- 숫자 블록
- 코드 계약
- jwt 표준
- piplining
- 프로그래머스
- 쿠키
- 알람 시스템
- 셀러리
- 트랜잭샨
- 깊게 생각해보기
- 수신자 대상 다르게
- 누적합
- AWS
- prg 패턴
- 객체지향패러다임
- 완전탐색
- 결제서비스
- 카카오
- BFS
- 이분탐색
- Today
- Total
코딩관계론
디자인 패턴 - 싱글톤 패턴 본문
싱글톤 패턴을 사용하는 이유
웹 애플리케이션은 일반적으로 여러 고객이 동시에 request 요청을 보낸다. 만약 우리가 각 요청마다 새로운 Service 객체와 Controller 객체를 생성하고, 이러한 객체들이 Garbage Collector에 의해 소멸된다면 이는 자원의 낭비를 초래할 수 있다.
따라서 객체를 미리 생성하고, 이 객체를 공유하는 방식으로 사용하게 된다면 사용자의 요청과 상관없이 하나의 객체만 사용자의 요청을 처리하게 될 것이다.
이러한 비효율성을 해결하기 위해 싱글톤 패턴이 등장하게 되었다.
싱글톤 패턴 구현 방법
1. 객체 생성자를 private로 선언하자
싱글톤은 객체를 하나만 생성하기 때문에 다른 곳에서 싱글톤 객체를 생성하지 못하게 해야 한다. 이를 위해 생성자를 private로 정의하여 다른 곳에서의 생성을 방지한다.
2. static필드로 싱글톤 객체를 생성해 두자
static 변수로 선언하는 이유는 클래스가 메모리에 로드될 때 단 한 번만 생성되며, 모든 인스턴스가 공유하기 때문에 싱글톤 객체를 보장할 수 있기 때문이다. `static` 변수로 선언된 객체는 일반적으로 애플리케이션이 종료되거나 클래스가 업로드되지 않는 한 GC의 대상이 되지 않아 소멸되지 않는다.
3. 사용은 getInstance 함수를 호출하자
getInstance() 함수를 통해서 싱글톤 객체를 불러올 수 있도록 한다. 이 메서드는 클래스의 유일한 인스턴스를 반환하며, 필요시 객체를 생성한다.
아래의 코드가 세 가지 규칙을 통해서 생성된 싱글톤 객체이다. 이후에 테스트 코드를 통해서 검증해 보면 객체의 주소가 같은 것을 확인할 수 있다.
package hello.core.sigleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService();
private SingletonService(){
}
public static SingletonService getInstance() {
return instance;
}
}
@Test
@DisplayName("싱글톤 객체 생성")
void singletonTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
Assertions.assertSame(singletonService1, singletonService2);
}
싱글톤 패턴의 주의점
싱글톤 패턴은 반드시 무상태로 설계해야 한다. 따라서 특정 클라이언트가 공유 변수의 값을 읽기만 가능하지, 공유변수에 값을 write 하면 안 된다. 따라서 공유되지 않는 변수(지역변수), thread local 등을 사용해야 한다.
public class SingletonStatefulService {
private int price; // 상태를 저장하는 필드
public void order(String user, int price) {
System.out.println("User: " + user + ", Price: " + price);
this.price = price; // 여기서 상태가 변경됨
}
public int getPrice() {
return price;
}
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SingletonStatefulServiceTest {
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
SingletonStatefulService singletonStatefulService1 = ac.getBean(SingletonStatefulService.class);
SingletonStatefulService singletonStatefulService2 = ac.getBean(SingletonStatefulService.class);
Runnable task1 = () -> {
singletonStatefulService1.order("userA", 10000);
System.out.println("User A ordered, price: " + singletonStatefulService1.getPrice());
};
Runnable task2 = () -> {
singletonStatefulService2.order("userB", 20000);
System.out.println("User B ordered, price: " + singletonStatefulService2.getPrice());
};
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
// 최종 가격 출력 (경합 상태로 인해 예상과 다를 수 있음)
System.out.println("Final price: " + singletonStatefulService1.getPrice());
}
}
위의 코드는 경합조건이 있어 항상 정당한 가격 출력을 보장할 수 없다. 따라서 아래와 같이 thread local을 이용하도록 변경해야 한다.
public class SingletonStatefulService {
private ThreadLocal<Integer> price = ThreadLocal.withInitial(() -> 0); // 상태를 ThreadLocal로 저장
public void order(String user, int price) {
System.out.println("User: " + user + ", Price: " + price);
this.price.set(price); // 여기서 상태가 변경됨
}
public int getPrice() {
return price.get();
}
}
싱글톤 패턴의 문제점
싱글톤 패턴은 많은 장점을 가지고 있지만, 다음과 같은 몇 가지 문제점도 있다:
- 설정 코드 필요
- 싱글톤 패턴을 구현하려면 생성자 필드 및 getInstance() 메서드를 추가로 작성해야 한다. 이는 코드의 복잡성을 증가시킬 수 있다.
- 클라이언트의 구체 클래스 의존
- 클라이언트가 싱글톤 인스턴스를 사용하기 위해 구체 클래스에 의존하게 되는데, 이는 의존성 역전 원칙(DIP)과 개방-폐쇄 원칙(OCP)을 위반하게 된다. 즉, 클라이언트 코드가 특정 싱글톤 클래스에 강하게 결합된다.
- 상속의 제한
- 싱글톤 패턴에서는 생성자를 private로 선언하기 때문에 자식 클래스를 만들 수 없다. 이는 객체 지향 설계의 상속 및 확장성을 제한한다.
- 유연성 저하
- 싱글톤 패턴은 클래스의 유연성을 저하시킬 수 있다. 예를 들어, 싱글톤 객체의 변경이 필요할 때, 코드의 많은 부분을 수정해야 할 수도 있다.
스프링에서의 싱글톤은?
스프링 프레임워크에서는 싱글톤 패턴을 보다 안전하고 효율적으로 구현할 수 있는 메커니즘을 제공한다. 이는 스프링의 의존성 주입과 컨테이너 관리 메커니즘 덕분에 가능하다. 스프링이 어떻게 이러한 문제점을 해결하는지와 관련된 예제를 통해 설명해보겠다.
싱글톤 문제 해결 방법
- @Configuration 애노테이션
- @Configuration 애노테이션은 스프링 컨테이너에게 해당 클래스가 하나 이상의 @Bean 메서드를 제공하고, 이 메서드들을 통해 스프링 빈을 정의함을 알린다. 이 클래스는 스프링에 의해 프락시 객체로 변환되어 싱글톤을 보장한다.
- CGLIB를 사용한 프록시 생성
- 스프링은 @Configuration 클래스를 상속한 프록시 클래스를 생성하고, 이를 통해 싱글톤 빈을 관리한다. 이를 통해 각 빈들이 프록시 클래스를 통해 제공되므로 싱글톤이 유지된다.
아래의 코드를 보면 각 객체들은 세 번이 생성되는 것이 자명해 보인다. 하지만 이 예제에서 memberService와 orderService는 동일한 MemoryMemberRepository 인스턴스를 참조한다. 이는 스프링이 @Configuration 클래스를 프락시로 변환하여 각 @Bean 메서드를 호출할 때마다 동일한 빈을 반환하도록 보장하기 때문이다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
}
@Bean
public MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy getDiscountPolicy() {
return new RateDiscountPolicy();
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SingletonTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
OrderService orderService = ac.getBean(OrderService.class);
MemoryMemberRepository memberRepository1 = (MemoryMemberRepository) ac.getBean("getMemberRepository");
MemoryMemberRepository memberRepository2 = memberService.getMemberRepository();
MemoryMemberRepository memberRepository3 = orderService.getMemberRepository();
System.out.println("memberService.getMemberRepository() = " + memberRepository2);
System.out.println("orderService.getMemberRepository() = " + memberRepository3);
System.out.println("getMemberRepository bean = " + memberRepository1);
// 모든 빈들이 동일한 인스턴스를 참조함
System.out.println(memberRepository1 == memberRepository2); // true
System.out.println(memberRepository2 == memberRepository3); // true
}
}
출력 결과
memberService.getMemberRepository() = hello.core.member.MemoryMemberRepository@38604b81
orderService.getMemberRepository() = hello.core.member.MemoryMemberRepository@38604b81
getMemberRepository bean = hello.core.member.MemoryMemberRepository@38604b81
true
true
스프링의 마법: CGLIB를 통한 프록시 생성
스프링이 @Configuration 클래스를 처리하는 방식은 다음과 같다:
- 스프링은 @Configuration 클래스를 상속하는 CGLIB 프록시 클래스를 생성한다.
- 이 프록시 클래스는 각 @Bean 메소드 호출을 가로채어, 이미 생성된 빈이 있으면 이를 반환하고, 없으면 새로 생성하여 반환한다.
- 이를 통해 각 빈은 싱글톤으로 관리되며, 애플리케이션 전반에 걸쳐 동일한 인스턴스가 사용된다.
순수한 클래스 사용 시 문제점
만약 @Configuration 애노테이션을 사용하지 않고, 순수한 Java 클래스를 사용할 경우, 각 @Bean 메소드 호출 시마다 새로운 인스턴스가 반환되어 싱글톤이 보장되지 않는다. 이는 아래와 같은 결과를 초래한다
memberService.getMemberRepository() = hello.core.member.MemoryMemberRepository@3e2055d6
orderService.getMemberRepository() = hello.core.member.MemoryMemberRepository@50029372
false
위와 같이 각 @Bean 메소드 호출 시 새로운 객체가 생성되어, 싱글톤이 보장되지 않는 것을 확인할 수 있다.
'개발 > Java' 카테고리의 다른 글
디자인 패턴 - 팩토리 패턴 (0) | 2024.07.08 |
---|---|
디자인패턴 - 어댑터 패턴 (0) | 2024.07.08 |
클러스터링과 논클러스트링이란? (0) | 2024.07.03 |
DB Connection Pool은 왜 필요할까? (0) | 2024.07.02 |
인터페이스를 통해 DB를 표준화했다고?(JDBC) (0) | 2024.07.01 |