Christmas Pikachu 자바 스프링부트 웹 개발에서 중요한 디자인 패턴과 활용
개발일지/스프링

자바 스프링부트 웹 개발에서 중요한 디자인 패턴과 활용

ZI_CO 2024. 10. 29.

스프링부트는 자바로 웹 애플리케이션을 개발할 때 널리 사용되는 프레임워크이며, 그 안에서 다양한 디자인 패턴이 활용됩니다. 이러한 디자인 패턴들은 코드의 재사용성, 유지 보수성, 그리고 가독성을 향상시켜주기 때문에 웹 애플리케이션 개발에 있어서 매우 중요합니다.

 

1. 싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 애플리케이션 내에서 클래스의 인스턴스를 하나만 생성하도록 보장하는 디자인 패턴입니다. 예를 들어, 데이터베이스 연결 객체를 여러 개 만드는 것은 비효율적이므로, 하나만 만들어서 재사용하고 싶을 때 싱글톤 패턴을 사용합니다.

  • 사용 사례: 데이터베이스 연결 풀, 설정 정보 등을 싱글톤으로 구현하여 전체 애플리케이션에서 하나의 인스턴스를 공유합니다.
  • 장점: 객체를 하나만 생성하므로 메모리 사용을 절약할 수 있고, 코드에서 같은 객체를 계속 사용할 수 있습니다.
  • 스프링에서의 구현: 스프링에서 @Component@Service 같은 애노테이션을 사용하면 자동으로 해당 클래스의 인스턴스를 싱글톤으로 관리합니다.
@Service
public class SingletonService {
    private static SingletonService instance;
    
    private SingletonService() {
        // private constructor - 외부에서 인스턴스 생성 불가
    }
    
    public static SingletonService getInstance() {
        if (instance == null) {
            instance = new SingletonService();
        }
        return instance;
    }
}

위 예제에서 getInstance() 메서드는 오직 하나의 인스턴스만 생성하여 공유하는 역할을 합니다. 이로써 SingletonService 객체를 하나만 유지합니다.

2. 팩토리 패턴 (Factory Pattern)

팩토리 패턴은 객체를 생성하는 로직을 별도의 클래스에 두어, 객체 생성의 복잡성을 숨기고 코드의 결합도를 줄이는 데 사용됩니다. 예를 들어, 특정 조건에 따라 다른 클래스의 인스턴스를 생성해야 할 때 팩토리를 사용하면 유용합니다.

  • 사용 사례: 다양한 종류의 객체(예: 여러 종류의 결제 방식)를 쉽게 생성하고 사용할 때 유용합니다.
  • 장점: 객체 생성 로직을 한 곳에 모아서 관리할 수 있어 유지 보수가 쉬워집니다.
  • 스프링에서의 구현: FactoryBean 인터페이스나 별도의 팩토리 클래스를 만들어 빈을 생성할 수 있습니다.
public class ServiceFactory {
    public static Service getService(String type) {
        if ("A".equals(type)) {
            return new ServiceA();
        } else if ("B".equals(type)) {
            return new ServiceB();
        } else {
            throw new IllegalArgumentException("Unknown service type");
        }
    }
}

public interface Service {
    void execute();
}

public class ServiceA implements Service {
    @Override
    public void execute() {
        System.out.println("Service A 실행");
    }
}

public class ServiceB implements Service {
    @Override
    public void execute() {
        System.out.println("Service B 실행");
    }
}

위 예제에서 ServiceFactory 클래스는 getService() 메서드를 통해 특정 타입에 맞는 객체를 반환합니다. 이로써 클라이언트 코드가 객체 생성에 대해 알 필요가 없도록 해줍니다.

3. 프록시 패턴 (Proxy Pattern)

프록시 패턴은 어떤 객체에 대한 접근을 제어하거나 추가적인 기능(예: 로깅, 캐싱 등)을 제공하기 위해 사용하는 패턴입니다. 스프링에서는 AOP(Aspect-Oriented Programming)라는 방식으로 메서드의 실행 전후에 특정 기능을 넣는 데 사용됩니다.

  • 사용 사례: 메소드 실행 전후로 로깅을 남기거나 권한을 검증하는 경우 사용됩니다.
  • 장점: 원본 코드의 수정 없이 부가 기능을 쉽게 추가할 수 있습니다.
  • 스프링에서의 구현: @Transactional, @Cacheable 등의 애노테이션을 사용하여 스프링이 자동으로 프록시를 만들어줍니다.
@Component
public class ProxyService {
    public void doAction() {
        System.out.println("실제 서비스 동작");
    }
}

public class ProxyServiceProxy extends ProxyService {
    @Override
    public void doAction() {
        System.out.println("프록시: 작업 전 처리");
        super.doAction();
        System.out.println("프록시: 작업 후 처리");
    }
}

프록시 클래스인 ProxyServiceProxy는 실제 서비스 클래스인 ProxyService의 동작 전후에 추가적인 로직을 넣어줍니다.

4. 의존성 주입 패턴 (Dependency Injection Pattern)

의존성 주입은 객체가 사용할 다른 객체를 직접 생성하지 않고, 외부에서 주입받도록 하는 패턴입니다. 이렇게 하면 클래스 간의 결합도를 줄이고, 테스트가 용이해집니다.

  • 사용 사례: 클래스가 여러 다른 클래스에 의존할 때, 이 의존성을 외부에서 관리하며 쉽게 교체할 수 있도록 합니다.
  • 장점: 객체 간 결합도가 낮아지고, 테스트 시 모의 객체(Mock)를 쉽게 주입할 수 있습니다.
  • 스프링에서의 구현: @Autowired, 생성자 주입 등을 사용하여 의존성을 관리합니다.
@Component
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void performAction() {
        userRepository.save();
    }
}

@Repository
public class UserRepository {
    public void save() {
        System.out.println("사용자 저장");
    }
}

위 예제에서 UserServiceUserRepository에 의존하고 있으며, 이 의존성을 스프링이 자동으로 주입해줍니다.

5. 템플릿 메소드 패턴 (Template Method Pattern)

템플릿 메소드 패턴은 알고리즘의 구조를 상위 클래스에서 정의하고, 하위 클래스에서 특정 단계를 구현하도록 하는 패턴입니다. 공통된 로직은 상위 클래스에 두고, 변화하는 부분만 하위 클래스에서 구현합니다.

  • 사용 사례: 공통적인 흐름은 동일하되, 세부 동작이 달라야 하는 경우 사용됩니다.
  • 장점: 코드의 중복을 줄이고, 비슷한 구조의 코드에서 변화되는 부분만 관리할 수 있습니다.
  • 스프링에서의 구현: JdbcTemplate, RestTemplate 같은 클래스에서 공통적인 데이터 처리 흐름을 구현합니다.
public abstract class DataProcessor {
    // 템플릿 메소드
    public void process() {
        readData();
        processData();
        writeData();
    }

    protected abstract void readData();
    protected abstract void processData();
    protected abstract void writeData();
}

public class CsvDataProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("CSV 데이터 읽기");
    }

    @Override
    protected void processData() {
        System.out.println("CSV 데이터 처리");
    }

    @Override
    protected void writeData() {
        System.out.println("CSV 데이터 쓰기");
    }
}

위 예제에서 DataProcessor 클래스는 데이터 처리의 공통된 흐름을 정의하고, 하위 클래스 CsvDataProcessor에서 각 단계를 구체적으로 구현합니다.

6. 전략 패턴 (Strategy Pattern)

전략 패턴은 여러 알고리즘을 정의하고 각각을 캡슐화하여, 필요에 따라 런타임에 알고리즘을 변경할 수 있게 하는 패턴입니다.

  • 사용 사례: 결제 방법이나 정렬 알고리즘처럼, 상황에 따라 다른 전략을 사용해야 할 때 유용합니다.
  • 장점: 알고리즘을 쉽게 교체할 수 있고, 코드의 유연성이 높아집니다.
  • 스프링에서의 구현: 전략을 스프링 빈으로 정의하고, 런타임에 주입받아 사용할 수 있습니다.
public interface PaymentStrategy {
    void pay(int amount);
}

@Component
public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 신용카드로 결제합니다.");
    }
}

@Component
public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 페이팔로 결제합니다.");
    }
}

@Component
public class PaymentService {
    private final PaymentStrategy paymentStrategy;

    @Autowired
    public PaymentService(@Qualifier("creditCardPayment") PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void executePayment(int amount) {
        paymentStrategy.pay(amount);
    }
}

위 코드에서 PaymentService는 주입받은 전략을 사용하여 결제를 처리하며, 전략은 런타임에 쉽게 변경될 수 있습니다.

7. 옵저버 패턴 (Observer Pattern)

옵저버 패턴은 객체의 상태 변화가 있을 때 그 변화를 다른 객체들(옵저버)에게 알리는 패턴입니다. 주로 이벤트 기반 시스템에서 사용됩니다.

  • 사용 사례: 특정 이벤트가 발생했을 때 여러 객체에게 이를 알리는 경우에 사용됩니다. 예를 들어, 사용자에게 이메일 알림을 보낼 때 사용됩니다.
  • 장점: 객체 간의 결합도를 낮추고, 이벤트 기반의 처리를 쉽게 할 수 있습니다.
  • 스프링에서의 구현: 스프링 이벤트(ApplicationEventPublisher@EventListener)를 사용하여 쉽게 구현할 수 있습니다.
@Component
public class EventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    public EventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void publishEvent(String message) {
        applicationEventPublisher.publishEvent(new CustomEvent(this, message));
    }
}

public class CustomEvent extends ApplicationEvent {
    private final String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Component
public class EventListenerBean {
    @EventListener
    public void handleCustomEvent(CustomEvent event) {
        System.out.println("이벤트 수신: " + event.getMessage());
    }
}

위 예제에서 EventPublisher는 이벤트를 발행하고, EventListenerBean은 해당 이벤트를 듣고 처리합니다.

마무리

이와 같이 스프링부트에서 자주 사용되는 디자인 패턴들은 애플리케이션의 확장성, 유지 보수성, 그리고 코드의 가독성을 높이는 데 큰 도움을 줍니다. 추가적으로 전략 패턴과 옵저버 패턴을 포함하여, 다양한 디자인 패턴을 적절히 이해하고 활용하는 것은 스프링부트를 이용한 웹 개발에서 매우 중요한 요소입니다.

댓글