스프링에서 자주 쓰이는 디자인패턴

kmb·2023년 5월 17일
0

스프링

목록 보기
8/9
post-thumbnail

디자인 패턴

프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계등을 이용하여 해결할 수 있도록 하나의 규악 형태로 만들어 놓은 것.

 

Spring에서 자주 사용되는 디자인 패턴

1. 싱글턴 패턴(Singleton Pattern)

애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴.
즉 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴.
따라서 애플리케이션 전역에서 공유해야 하는 객체의 경우나 데이터베이스 연결모듈에 주로 사용.

스프링 컨테이너가 싱글톤 빈을 구현.
특정 클래스에서 @Bean이 정의되면 스프링 컨테이너는 해당 클래스에 대한 1개의 공유 인스턴스를 만든다. 이후에 Bean이 호출될 때마다 공유 인스턴스를 반환.

단점 : TDD(Test Driven Development)를 할 때 테스트마다 독립적인 인스턴스를 만들기 어렵다.

 

  • ex) 싱글턴 패턴 예시
/*
MySingletonService 클래스는 @Service 어노테이션에 의해 스프링의 Bean으로 등록되며, 
String 타입의 message 필드와 getter/setter 메서드를 가지고있다.
*/

@Service
public class MySingletonService {  // 싱글톤으로 사용되는 서비스 클래스
    
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
/*
OtherService 클래스는 생성자를 통해 MySingletonService 클래스를 주입받았고 싱글톤으로 관리되는 MySingletonService 인스턴스를 사용할 수 있다.

OtherService 가 여러 번 생성되더라도 항상 동일 한 MySingletonService 인스턴스가 주입된다.
*/

@Service
public class OtherService {

    private final MySingletonService singletonService;

    public OtherService(MySingletonService singletonService) {
        this.singletonService = singletonService;
    }

    public void doSomething() {
        String message = singletonService.getMessage();
        // MySingletonService 인스턴스 사용
        // ...
    }
}

2. 프록시 패턴(Proxy Pattern)
어떤 객체에 대한 접근을 제어하기 위한 용도로, 실제 객체의 메서드를 호출하면 직접적으로 해당 객체를 참조하는것이 아니라, 해당 객체를 대리하는 객체를 통해서 접근하는 방식.

스프링에서는 프록시 패턴을 기반으로 하는 AOP(Aspect-Oriented Programming)를 통해서
주요한 기능인 메서드 호출 전후에 공통 로직을 삽입할 수 있다.
이를 통해서 애플리케이션에 반복되는 부가적인 작업(로깅, 트랜잭션 관리)을 분리해서 모듈화 할 수 있다.

 

  • ex) 프록시 패턴 예시
/*
MyServiceProxy 클래스는 실제 서비스객체인 MyServiceImpl 클래스를 감싸고 있고, 
메서드 호출 시 추가적인 로직을 적용할 수 있다.
MyServiceProxy 클래스의 doSomething() 메서드는 MyServiceImpl 클래스의 doSomething() 메서드를 호출하기 전후 실행시간을 측정해서 로깅한다.
*/

public interface MyService {
    void doSomething();
}

@Component
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // 실제 작업 수행
        System.out.println("Doing something...");
    }
}

@Component
public class MyServiceProxy implements MyService {
    private final MyService myService;

    public MyServiceProxy(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void doSomething() {
        long startTime = System.currentTimeMillis();
        myService.doSomething();
        long endTime = System.currentTimeMillis();

        long executionTime = endTime - startTime;
        System.out.println("Execution time: " + executionTime + "ms");
    }
}

@Component
public class MyClient {
    private final MyService myService;

    public MyClient(MyService myService) {
        this.myService = myService;
    }

    public void performTask() {
        myService.doSomething();
    }
}

MyClient 클래스에서 MyService 클래스를 주입받고 performTask() 메서드를 호출하면 프록시를 통해 추가적인 로깅이 수행되고 실행시간이 출력된다.

3. 템플릿 메서드 패턴(Template Method Pattern)

슈퍼클래스에 기본적인 로직의 흐름을 작성하고, 일부 변경이 필요한 부분은 서브클래스에서 추상메소드로 오버라이딩하여 사용할 수 있는 패턴.

스프링에서는 템플릿 메서드 패턴을 기반으로 JdbcTemplate을 통해 데이터베이스 엑세스를 한다.

 

  • ex) 템플릿 메서드 패턴 예시
/*
JdbcDaoTemplate 추상클래스의 query() 메서드에서 데이터베이스 연결을 설정하고
SQL쿼리를 실행하며 mapRow() 추상메서드를 호출해서 결과셋을 처리.
이후 JdbcDaoTemplate 클래스를 상속받은 MyDao 서브클래스에서 mapRow() 메서드를 구현해서 결과셋을 매핑하는 로직을 추가.
*/

public abstract class JdbcDaoTemplate {
    private final DataSource dataSource;

    public JdbcDaoTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    protected abstract void mapRow(ResultSet rs, int rowNum) throws SQLException;

    public void query(String sql) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            conn = dataSource.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery(sql);

            int rowNum = 0;
            while (rs.next()) {
                mapRow(rs, rowNum);
                rowNum++;
            }
        } catch (SQLException e) {
            // 예외 처리
        } finally {
            // 리소스 해제
        }
    }
}

public class MyDao extends JdbcDaoTemplate {
    public MyDao(DataSource dataSource) {
        super(dataSource);
    }

    @Override
    protected void mapRow(ResultSet rs, int rowNum) throws SQLException {
        // 결과셋 매핑 로직 구현
    }
}

public class MyApp {
    public static void main(String[] args) {
        DataSource dataSource = new MyDataSource(); // DataSource 생성
        MyDao dao = new MyDao(dataSource); // MyDao 생성

        dao.query("SELECT * FROM my_table"); // 데이터베이스 쿼리 수행
    }
}

MyApp 클래스에서 MyDao 인스턴스를 생성하고 query() 메서드를 호출해서 데이터베이스 엑세스를 진행.

4. 팩토리 메서드 패턴(Factory Method Pattern)

객체 생성을 서브 클래스로 분리하여 캡슐화 처리하도록 하는 디자인패턴.

즉 클라리언트에서 new 연산자를 통해 객체를 생성하는것이 아닌 별도의 클래스와 메서드를 만들어
팩토리모듈화하여 구체적인 부분이 아닌 추상적인 부분에 의존하도록 한다.

 

  • ex) 템플릿 메서드 패턴 예시
/*
스프링에서 제공하는 인터페이스인 FactoryBean을 이용해서 Bean을 생성하고 구성한다.

getObject() 메서드에서 MyBeanImpl 인스턴스를 생성해서 반환.
getObjectType() 메서드에서 팩토리가 생성하는 Bean의 타입을 반환.
isSingleton() 메서드에서 생성되는 Bean이 싱글톤인지 확인.
*/

public interface MyBean {
    void doSomething();
}

public class MyBeanImpl implements MyBean {
    @Override
    public void doSomething() {
        System.out.println("MyBeanImpl doSomething()");
    }
}

public class MyBeanFactory implements FactoryBean<MyBean> {
    @Override
    public MyBean getObject() throws Exception {
        return new MyBeanImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return MyBean.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}
/*
AppConfig 클래스에서 myBean() 메서드를 @Bean으로 정의하여 MyBean을 등록.
MyBeanFactory 클래스의 getObject() 메서드를 호출하여 MyBean 인스턴스를 반환.

MyService 클래스는 MyBean을 주입받아서 사용.
execute() 메서드에서 MyBean 인터페이스의 doSomething() 메서드를 호출하여 동작.
*/

@Configuration
public class AppConfig {
    @Bean
    public MyBean myBean() throws Exception {
        return new MyBeanFactory().getObject();
    }
}

@Service
public class MyService {
    private final MyBean myBean;

    public MyService(MyBean myBean) {
        this.myBean = myBean;
    }

    public void execute() {
        myBean.doSomething();
    }
}

위 예시처럼 팩토리 메서드 패턴을 사용하여 객체 생성과 구성을 팩토리 클래스에 위임하여 동적인 Bean 생성이 가능하다.


5. 전략 패턴(strategy pattern)

비슷한 동작을 하지만 다르게 구현되어 있는 객체의 행위(전략)들을 공통의 인터페이스를 구현하는 각각의 클래스로 구현하고, 동적으로 바꿀 수 있도록 하는 패턴.
직접 행위에 대한 코드를 수정할 필요 없이 전략만 변경하여 유연하게 확장가능.

스프링에서 DI (의존관계 주입)시 사용하는 패턴이다.

 

  • ex) 전략 패턴 예시
public interface SoundStrategy {
    void makeSound();
}

public class DogSoundStrategy implements SoundStrategy {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
}

public class CatSoundStrategy implements SoundStrategy {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

public class Animal {
    private SoundStrategy soundStrategy;
    
    public Animal(SoundStrategy soundStrategy) {
        this.soundStrategy = soundStrategy;
    }
    
    public void makeSound() {
        soundStrategy.makeSound();
    }
}

public class Main {
    public static void main(String[] args) {
        SoundStrategy dogStrategy = new DogSoundStrategy();
        Animal dog = new Animal(dogStrategy);
        dog.makeSound(); // 멍멍 출력
        
        SoundStrategy catStrategy = new CatSoundStrategy();
        Animal cat = new Animal(catStrategy);
        cat.makeSound(); // 야옹 출력
    }
}

생성자주입을 통해 SoundStrategy 인터페이스의 구현 객체들인 DogSoundStrategy 클래스와 CatSoundStrategy 클래스를 주입시킨다.

profile
꾸준하게

0개의 댓글