네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'2022/06'에 해당되는 글 9건


반응형

컨트롤러에서 발생한 중복 코드

컨트롤러에서 반복되는 로직을 실행해야하는 경우가 있다. 해당 엔드포인트에 대한 인증(Authentication)인가(Authorization) 기능이 대표적인 예이다. 아래 코드는 인증을 구현하기 위해 컨트롤러의 여러 메서드에서 중복 코드가 발생한 예시이다. 

 

@GetMapping("/me")
public ResponseEntity<String> getMyInfo(@RequestHeader("Authorization") String token) {

    // 인증이 필요한 컨트롤러 메서드마다 등장하는 중복된 인증 로직
    if (!authService.validateToken(token)) {
        throw new AuthException();
    }
    
    // 유저 정보를 가져오는 로직 (생략)
    
    return ResponseEntity.ok("유저 정보");
}

@PatchMapping("/me")
public ResponseEntity<String> updateMyInfo(@RequestHeader("Authorization") String token) {

    // 인증이 필요한 컨트롤러 메서드마다 등장하는 중복된 인증 로직
    if (!authService.validateToken(token)) {
        throw new AuthException();
    }
    
    // 유저 정보를 수정하는 로직 (생략)
    
    return ResponseEntity.noContent().build();
}

@DeleteMapping("/me")
public ResponseEntity<String> deleteMyInfo(@RequestHeader("Authorization") String token) {

    // 인증이 필요한 컨트롤러 메서드마다 등장하는 중복된 인증 로직
    if (!authService.validateToken(token)) {
        throw new AuthException();
    }
    
    // 유저 정보를 삭제하는 로직 (생략)
    
    return ResponseEntity.noContent().build();
}

혹여 인증이 필요한 엔드포인트에서 인증 로직을 실수로 누락해버린다면, 큰 문제가 발생할 것 이다. 코드에서 중복은 해악이다.

 

Spring Interceptor

우리는 이 문제를 인터셉터(Interceptor)로 해결할 수 있다. 여러 컨트롤러에서 같은 관심사를 갖고 반복되어 사용하는 코드를 제거하고, 다수의 컨트롤러에 동일한 기능을 제공하기 위해 사용하는 것이 인터셉터이다.

 

인터셉터 호출 흐름

스프링의 인터셉터 동작은 크게 '컨트롤러 실행 전'/ '컨트롤러 실행 후, 뷰 실행 전'/ '뷰 실행 후' 이 세단계로 구분된다. 스프링 인터셉터를 만들기 위해서는 HandlerInterceptor 인터페이스를 구현해야 하는데, 해당 인터페이스는 preHandle(), postHandle(), afterCompletion()이라는 세 메서드를 제공한다.

 

Interceptor work with Spring MVC

1. preHandle()

컨트롤러 호출 전에 호출되는 메서드다. preHandle()의 반환타입은 boolean이다. 만약 preHandle()이 false를 반환한다면, 다음 HandlerInterceptor 혹은 컨트롤러를 실행하지 않는다.

 

2. postHandle()

컨트롤러가 정상적으로 실행된 이후에 실행되는 메서드다. 컨트롤러에서 예외가 발생한다면, postHandle() 메서드는 실행되지 않는다.

 

3. afterCompletion()

뷰가 클라이언트 응답을 전송한 뒤에 실행된다. 컨트롤러 실행과정에서 예외가 발생한 경우 해당 예외가 afterCompletion() 메서드의 4번째 파라미터로 전달되어 로그를 남기는 등 후처리를 위해 사용될 수 있다.

 

인터셉터 작성

HandlerInterceptor 인터페이스를 구현하는 SomeInterceptor라는 클래스를 작성하여 직접 스프링의 인터셉터를 사용해보자.

public class SomeInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 
        return true;
    }
    
    @Override
    public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { 
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
    }
}

인터셉터 등록

WebMvcConfigurer 인터페이스를 구현하면 Spring MVC의 설정을 할 수 있다고 한다. 

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SomeInterceptor());
    }
}

 

인터셉터를 활용하여 인증 구현하기

이제 인터셉터가 대략 어떤 식으로 동작하는지 파악했다. 우리는 맨 처음의 코드에서 AuthService를 통해 토큰을 검증하는 로직과 유효하지 않은 토큰에 대한 응답을 반환하는 부분이 중복되었음을 확인했다.

@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final AuthService authService;
    
    public AuthInterceptor(AuthService authService) {
        this.authService = authService;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        
        if (!authService.validateToken(token)) {
            throw new AuthException();
        }
        
        return true;
    }
}

preHandle() 메서드에서 HTTP Header를 통해 가져온 토큰을 AuthService의 validateToken()을 통해 검증하고, 유효하지 않은 토큰이 들어왔다면 예외를 발생시킨다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;
    
    public WebConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor);
    }
}

WebConfig는 빈으로 등록된 AuthInterceptor를 주입받아 인터셉터로 등록한다.

 

Interceptor 설정하기

그런데 만약 접근시 인증이 필요없는 엔드포인트가 존재한다면 어떻게 될까? 인터셉터는 현재 모든 컨트롤러 메서드에 대해 동작하고 있다. 특정 엔드포인트만 적용하거나, 제외하고 싶다면 별도의 설정이 필요하다.

 

WebConfig 클래스에서 인터셉터 등록을 위해 InterceptorRegistry의 addInterceptor() 메서드를 실행했다. 이 메서드는 InterceptorRegistation 타입의 객체를 반환하는데, 해당 객체는 addPathPatterns()와 excludePathPatterns()라는 메서드를 제공한다. 이 메서드는 Ant Pattern이라는 경로 패턴을 사용하여 인터셉터를 등록할 경로를 설정해줄 수 있다.

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(authInterceptor)
            .addPathPatterns("/users/*");
}

 

[ Ant Pattern ]

Ant Pattern은 *, **, ? 이 3개의 특수문자를 이용하여 경로를 표현한다. 각 문자는 아래의 의미를 갖는다. 

 

* : 0개 이상의 글자와 매칭

/users/*는 /users/me, /users/123 와는 매칭되지만, /users/123/items와는 매칭이 되지 않는다.

 

** : 0개 이상의 디렉터리 혹은 파일과 매칭

/users/**  /users/me, /users/342, /users/342/favorites 와 모두 매칭된다. 또한 /a/**/z  /a/b/c/d/e/z 와 매칭된다.

 

? : 1개 글자와 매칭

/u?ers 는 /users, /uzers 와 매칭되지만 /ussers와는 매칭되지 않는다.

 

 

 출처 

https://hudi.blog/spring-handler-interceptor/

 

Spring HandlerInterceptor를 활용하여 컨트롤러 중복 코드 제거하기

우아한테크코스 레벨2 마지막 미션인 장바구니 미션에서 인증과 인가를 구현하기 위해, Spring Interceptor 를 사용해야했다. 이를 위해 학습한 내용을 정리해보았다. 컨트롤러에서 발생한 중복 코드

hudi.blog

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

JPA를 이용하여 엔티티 객체들을 Builder 기반으로 생성하는 것은 흔한 패턴이다.

 

Builder로 안전하게 생성하자

JPA 엔티티 객체들을 Builder 기반으로 생성하는 것이 흔한 패턴이다. 

 

Builder 패턴의 장점

1. 인자가 많을 경우 쉽고 안전하게 객체를 생성할 수 있다.

2. 인자의 순서와 상관없이 객체를 생성할 수 있다.

3. 적절한 책임을 이름에 부여하여 가독성을 높일 수 있다.

 

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {
    
    @NotEmpty @Column(name = "bank_name", nullable = false)
    private String bankName;
    
    @NotEmpty @Column(name = "account_number", nullable = false)
    private String accountNumber;
    
    @NotEmpty @Column(name = "account_holder", nullable = false) 
    private String accountHolder;
    
    // 불완전한 객체 생성 패턴
    @Builder
    public Account(String bankName, String accountNumber, String accountHolder) {
        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }
    
    // 안전한 객체 생성 패턴
    @Builder
    public Account(String bankName, String accountNumber, String accountHolder) {
        Assert.hasText(bankName, "bankName must not be empty");
        Assert.hasText(accountNumber, "accountNumber must not be empty");
        Assert.hasText(accountHolder, "accountHolder must not be empty");
        
        this.bankName = bankName;
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
    }
}

데이터베이스의 컬럼이 not null 인 경우에는 대부분 엔티티의 멤버값도 null이면 안된다. 

Account account = Account.builder().build();

불완전한 객체 생성 패턴으로 생성했을 경우 account 객체에는 모든 멤버 필드의 값이 null로 지정된다. account 객체로 추가적인 작업을 진행하면 NPE가 발생하게 된다.

안전한 객체 생성 패턴으로 생성했을 경우는 객체 생성이 Assert로 객체 생성이 진행되지 않는다. 필요한 값이 없는 상태에서 객체를 생성하면 이후 작업에서 예외가 발생하게 된다. 객체가 필요한 값이 없는 경우에는 적절하게 Exception을 발생시켜서 흐름을 종료하는 것이 맞다.

 

@Entity
@Table(name = "orders")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Order {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Embedded
  private Address address;

  @OneToMany(mappedBy = "order")
  private List<Product> products = new ArrayList<>();

  @Builder
  public Order(Address address, List<Product> products) {
    Assert.notNull(address, "address must not be null");
    Assert.notNull(products, "products must not be null");
    Assert.notEmpty(products, "products must not be empty");

    this.address = address;
    this.products = products;
  }
}

 

Builder 이름으로 책임을 부여하자

Builder의 이름을 명확하게 해서 책임을 부여하는 것이 좋다.

@Entity
@Table(name = "refund")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Refund {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Embedded
    private Account account;
    
    @Embedded
    private CreditCard creditCard;
    
    @OneToOne
    @JoinColumn(name = "order_id", nullable = false, updatable = false)
    private Order order;
    
    @Builder(builderClassName = "ByAccountBuilder", builderMethodName = "ByAccountBuilder")
    public Refund(Account account, Order order) {
        Assert.notNull(account, "account must not be null");
        Assert.notNull(order, "order must not be null");
        
        this.order = order;
        this.account = account;
    }
    
    @Builder(builderClassName = "ByCreditBuilder", builderMethodName = "ByCreditBuilder")
    public Refund(CreditCard creditCard, Order order) {
        Assert.notNull(creditCard, "creditCard must not be null");
        Assert.notNull(order, "order must not be null");
        
        this.order = order;
        this.creditCard = creditCard;
    }
}

빌더의 이름으로 책임을 명확하게 부여하고, 받아야 하는 인자도 명확하게 할 수 있다.

public class RefundTest {
  
  // 생략

  @Test
  public void ByAccountBuilder_test() {
    final Refund refund = Refund.ByAccountBuilder() // 빌더 이름
        .account(account)
        .order(order)
        .build();

    assertThat(refund.getAccount()).isEqualTo(account);
    assertThat(refund.getOrder()).isEqualTo(order);
  }

  @Test
  public void ByCreditBuilder_test() {
    final Refund refund = Refund.ByCreditBuilder() // 빌더 이름
        .creditCard(creditCard)
        .order(order)
        .build();

    assertThat(refund.getCreditCard()).isEqualTo(creditCard);
    assertThat(refund.getOrder()).isEqualTo(order);
  }

}

 

 출처 

https://cheese10yun.github.io/spring-builder-pattern/

 

Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그

Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그

cheese10yun.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

ModelMapper 라이브러리

 

의존성 추가

build.gradle

implementation 'org.modelmapper:modelmapper:2.4.2'

 

변환 클래스들 정의

// 모든 클래스 Constructor, Getter, Setter 생략
class Address {
    String street;
    String city;
}

class Name {
    String firstName;
    String lastName;
}

class Customer {
    Name name;
}

class Order {
    Customer customer;
    Address address;
}

class OrderDto {
    String customerFirstName;
    String customerLastName;
    String billingStreet;
    String billingCity;
}

 

ModelMapper.map(Object source, Class<D> destinationType)

Order order = new Order(
                new Customer(new Name("FIRSTNAME", "LASTNAME")),
                new Address("STREET", "CITY")
              );

ModelMapper modelMapper = new ModelMapper();

OrderDto result = modelMapper.map(order, OrderDto.class);
// result - customerFirstName = "FIRSTNAME"
// result - customerLastName = "LASTNAME"
// result - billingStreet = "STREET"
// result - billingCity = "CITY"

각 클래스 프로퍼티들의 연관관계를 자동으로 판단하여 매핑되었다.

 

이처럼 ModelMapper에서는 map(source, destination) 메서드가 호출되면 source와 destination의 타입을 분석하여 매칭 전략 및 기타 설정 값에 따라 일치하는 속성을 결정하게 된다. 그런 다음 결정한 매칭 항목들에 대해 데이터를 매핑하는 것이다.

위처럼 source와 destination의 객체 타입이나 프로퍼티가 다른 경우에도 설정된 매칭 전략에 따라서 최선의 매핑과정을 수행한다.

암시적으로 일치하는 프로퍼티들의 연관관계가 존재하지 않거나 이러한 연관관계가 모호한 경우, 매핑이 원활히 이루어지지 않을 수도 있다.

 

TypeMap

// 모든 클래스 Constructor, Getter, Setter 생략

class Item {
    private String name;
    private Integer stock;
    private Integer price;
    private Double discount;
}

class Bill {
    private String itemName;
    private Integer qty;
    private Integer singlePrice;
    private Boolean sale;
}
Item itemA = new Item("itemA", 10, 1500, true);
Bill bill = modelMapper.map(itemA, Bill.class);
// bill - itemName = "itemA"
//      - qty = null
//      - singlePrice = null
//      - discount = null

ModelMapper의 기본 매칭 전략으로는 모호한 연관관계들은 매핑되지 않는다.

 

따라서 ModelMapper에서는 위와 같은 문제를 해결하기 위해 Type Map 기능을 제공한다.

 

TypeMap<S, D>

TypeMap 인터페이스를 구현함으로써 매핑 설정을 캡슐화하여(Encapsulating)하여 ModelMapper 객체의 매핑 관계를 설정해줄 수 있다.

 

매핑 관계 추가

먼저 위의 예시에서 우리가 원하는 매핑 전략은 다음과 같다.

  • Item.stock -> Bill.qty
  • Item.price -> Bill.singlePrice
  • Item.sale -> Bill.discount

수량과 가격의 경우 아래와 같이 메서드 레퍼런스를 통해 간단히 설정 가능하다.

modelMapper.typeMap(Item.class, Bill.class).addMappings(mapper -> {
    mapper.map(Item::getStock, Bill::setQty);
    mapper.map(Item::getPrice, Bill::setSinglePrice);
});

Bill bill2 = modelMapper.map(itemA, Bill.class);
// bill2 - itemName = "itemA"
//       - qty = 10
//       - singlePrice = 1500
//       - discount = null

하지만 Item.sale, Bill.discount와 같이 클래스 타입이 다른 경우 추가적인 방법이 필요하다.

 

파라미터 타입 변환

매핑하려는 데이터의 source와 destination 타입이 다른 경우, Converter 인터페이스를 사용해 유연하게 값을 설정해줄 수 있다.

 

Item.sale == true일 경우 할인율 20.0으로 설정해준다고 가정하자

mapper.using(Converter<S, D>)와 같은 패턴을 사용하면 유연한 타입 변환이 가능하다. using은 말 그대로 다음과 같은 Converter 규칙을 사용하겠다는 것이다.

 

modelMapper.typeMap(Item.class, Bill.class).addMappings(mapper -> {
    mapper.map(Item::getStock, Bill::setQty);
    mapper.map(Item::getPrice, Bill::setSinglePrice);
    mapper.using((Converter<Boolean, Double>) context -> context.getSource() ? 20.0 : 0.0)
            .map(Item::isSale, Bill::setDiscount);
});

Bill bill2 = modelMapper.map(itemA, Bill.class);
// bill2 - itemName = "itemName"
//       - qty = 10
//       - singlePrice = 1500
//       - discount = 20.0

Converter를 통해 정상적으로 타입이 변환되었다.

 

매핑 skip하기

클래스의 특정 프로퍼티는 매핑이 이루어지지 않도록 설정하는 것도 가능하다.

modelMapper.typeMap(Item.class, Bill.class).addMappings(mapper -> {
    mapper.map(Item::getStock, Bill::setQty);
    mapper.map(Item::getPrice, Bill::setSinglePrice);
    mapper.using((Converter<Boolean, Double>) context -> context.getSource() ? 20.0 : 0.0)
            .map(Item::isSale, Bill::setDiscount);
    mapper.skip(Bill::setItemName); // skip 추가
});

Bill bill2 = modelMapper.map(itemA, Bill.class);
// bill2 - itemName = null
//       - qty = 10
//       - singlePrice = 1500
//       - discount = 20.0

Bill.itemName 값의 매핑이 임의로 스킵되었다.

 

null인 속성 값만 매핑 skip 하기

객체에 새로운 값들을 한번에 업데이트해줄 때, ModelMapper의 기본 매칭 전략을 사용하면 null까지 함께 업데이트가 되는 문제가 생기므로 이를 위해서 매핑 설정을 해줄 수 있다.

ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setSkipNullEnabled(true);

 

Validation

ModelMapper는 기본적으로 매칭 전략에 맞지 않는 속성들은 null 값으로 초기화하게 되는데 개발자의 입장에서 어떤 객체에 대해 모든 속성값들이 정상적으로 매핑되었는지 검증할 필요가 있다.

 

ModelMapper().validate()를 이용하여 매핑 검증이 실패하는 경우 예외 처리를 해주기 때문에 추가적인 예외 핸들링이 가능하다.

modelMapper = new ModelMapper();

Bill bill3 = modelMapper.map(itemA, Bill.class);

try {
    modelMapper.validate();
} catch (ValidatationException e) {
    // Exception Handling
}

 

Strategies

ModelMapper는 지능적인 오브젝트 매핑을 수행한다.

객체들의 매칭 전략을 하나하나씩 임의로 설정해 주어야 한다면 편의성을 위해서 ModelMapper 라이브러리를 사용하는 것이 아니게 되므로 특정 매칭 전략을 입력해 주지 않고도 다른 매칭 전략을 사용할 수 있게끔 추가적인 매칭 전략을 제공한다.

modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD); // STANDARD 전략
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE); // LOOSE 전략
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); // STRICT 전략

STANDARD

기본 매칭 전략으로서 STANDARD 전략을 사용하면 source 와 destination의 속성들을 지능적으로 매치시킬 수 있다.

  • 토큰은 어떤 순서로든 일치될 수 있다.
  • 모든 destination 속성 이름 토큰이 일치해야 한다.
  • 모든 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

위 조건들을 충족하지 못하는 경우 매칭에 실패하여 null이 입력된다.

 

LOOSE

느슨한 매칭 전략으로 계층 구조의 마지막 destination 속성만 일치하도록 요구하여 source와 destination을 느슨하게 매치할 수 있다.

  • 토큰은 어떤 순서로든 일치될 수 있다.
  • 마지막 destination 속성 이름에는 모든 토큰이 일치해야 한다.
  • 마지막 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

느슨한 일치 전략은 속성 계층 구조가 매우 다른 source, destination 객체에 사용하는데 이상적이다.

Order, OrderDto 와 같이 객체의 속성이 계층 구조를 가지는 경우

 

STRICT

엄격한 일치 전략으로, 불일치나 모호성이 발생하지 않도록 완벽한 일치 정확도를 얻을 수 있다. 하지만 source와 destination의 속성 이름들이 서로 정확하게 일치해야 한다.

  • 토큰은 엄격한 순서로 일치해야 한다.
  • 모든 destination 속성 이름 토큰이 일치해야 한다.
  • 모든 source 속성 이름에는 모든 토큰이 일치해야 한다.

STRICT 전략을 통해 앞에서 다룬 TypeMap 을 사용하지 않고도 모호함이나 예기치 않은 매핑이 발생하지 않도록 하는 경우에 간편하게 사용이 가능하다. 하지만 반드시 매칭되어야 하는 속성의 이름들이 서로 정확하게 일치해야 한다.

 

 

 

 출처 

https://devwithpug.github.io/java/java-modelmapper

 

ModelMapper 제대로 알고 사용하자!

개요

devwithpug.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

'객체지향의 4대 특성'은 객체지향을 잘 사용하기 위한 도구이다.

'객체지향의 5대 원칙'은 이러한 도구를 올바르게 사용하는 원칙으로 볼 수 있다.

'디자인 패턴'은 레시피에 비유할 수 있다.

실제 개발현장에서 비즈니스 요구사항을 처리하면서 만들어진 다양한 해결책 중 많은 사람들이 인정한 Best Practice를 정리한 것이다.

-> 스프링 역시 다양한 디자인 패턴을 활용하고 있다.

스프링 공식 정의: "자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크"

디자인 패턴은 객체 지향의 특성 중 '상속', '인터페이스', '합성'을 이용한다. (합성은 객체를 속성으로 사용하는 것)

 

어댑터 패턴 (Adapter Pattern)

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

OBDC/JDBC가 어댑터 패턴을 이용해 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해준다.

'어플리케이션' - '어댑터' - '실제 구현체'

 

어댑터 패턴 사용

public class AdapterServiceA {
    ServiceA serviceA = new ServiceA();
    
    void runService() {
        serviceA.runServiceA();
    }
}

public class AdapterServiceB {
    ServiceB serviceB = new ServiceB();
    
    void runService() {
        serviceB.runServiceB();
    }
}

// 어댑터 패턴이 적용된 코드
public class ClientWithAdapter {
    public static void main(String[] args) {
        AdapterServiceA adpaterA = new AdapterServiceA();
        AdapterServiceB adpaterB = new AdapterServiceB();
        
        adpaterA.runService();
        adpaterB.runService()
    }
}

어댑터에서 동일한 이름의 runService()를 사용해서 각각의 runServiceX를 호출하고 있다.

어댑터가 특정 인터페이스를 구현하게 해서 하나의 인터페이스에서 의존관계 주입을 통해 똑같은 메서드로 각각 다른 구현체의 메서드를 호출할 수 있다.

호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴

 

프록시 패턴 (Proxy Pattern)

프록시는 대리자라는 의미이다. 다른 누군가를 대신해 그 역할을 수행하는 존재를 말한다.

 

프록시 패턴 사용

public interface IService {
    String runSomething();
}

public class Service implements IService {
    public String runSomething() {
        return "서비스 최고!!";
    }
}

public class Proxy implements IService {
    IService service;
    
    public String runSomething() {
        System.out.println("호출에 대한 흐름 제어가 주 목적, 변환 결과를 그대로 전달");
        
        service = new Service();
        return service.runSomething();
    }
}

public class ClientWithProxy {
    public static void main(String[] args) {
        IService proxy = new Proxy();
        System.out.println(proxy.runSomething());
    }
}

서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입한다.

 

  • 프록시는 실제 서비스와 같은 이름의 메서드를 구현
  • 프록시는 실제 서비스에 대한 참조 변수를 갖는다. (합성)
  • 프록시는 실제 서비스의 메서드와 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 반환한다.
  • 프록스는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다.
프록시 패턴은 실제 서비스의 반환값은 변경하지 않고 제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용한다.

 

데코레이터 패턴 (Decorator Pattern)

원본에 장식을 더하는 패턴

프록시 패턴과 구현 방법이 같으나 최종적으로 반환값에 장식을 덧입힌다.

public class Decorator implements IService {

    IService service;
    
    public String runSomething() {
        System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
        
        service = new Service();
        
        return "정말" + service.runSomething(); // 반환값에 다른 값을 추가한다.
    }
}

데코레이터 패턴의 중요 포인트는 프록시 패턴의 중요 포인트에 반환값에 변화를 줄 수 있다는 점이다.

 

싱글톤 패턴 (Singleton Pattern)

인스턴스를 하나만 만들어 사용하기 위한 패턴

'커넥션 풀', '스레드 풀', '디바이스 설정 객체' 등과 같은 경우 인스턴스를 여러 개 만들게 되면 불필요한 자원을 사용하게 되고, 프로그램이 예상하지 못한 결과를 만들 수 있다.

 

싱글톤 패턴을 적용하면 두 개의 객체가 존재할 수 없으므로 

1. 객체 생성을 위한 new에 제약을 걸어야 하고

2. 만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다.

 

  • new 를 사용할 수 없도록 생성자에 private 접근 제어자를 설정한다.
  • 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.
  • 유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.
public class Singleton {
    static Singleton singletonObject; // 정적 참조 변수
    
    // private 생성자
    private Singleton {
    }
    
    // 객체 변환 정적 메서드
    public static Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        
        return singletonObject;
    }
}

 

 

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

상위 클래스의  견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴

공통적으로 사용하는 메서드는 상위 클래스에서 구현

하위 클래스마다 달라지는 것은 추상 클래스로 구현 강제화

하위 클래스마다 달라질 수 있는 것은 오버라이드 가능한 훅 메서드로 만듦.

 

public abstract class Animal {

    // 템플릿 메서드
    public void playWithOwner() {
        System.out.println("귀염둥이 이리온...");
        play();
        runSomething();
        System.out.println("잘했어!");
    }
    
    // 추상 메서드
    abstract void play();
    
    // Hook(갈고리) 메서드
    void runSomething() {
        System.out.println("꼬리 살랑 살랑-");
    }
}

상위 클래스에서 템플릿을 제공하는 playWithOwner() 템플릿 메서드를 제공한다.

그리고 템플릿 메서드 안에 있는 play() 추상 메서드와 runSomething() 메서드가 있다.

-> 추상 메서드는 하위 클래스에서 구현을 강제한다.

-> Hook 메서드는 오버라이드가 자유롭다.

 

템플릿 메서드: 공통 로직 수행, 로직 수행 중 추상메서드 혹은 훅 메서드를 호출

템플릿 메서드에서 호출하는 추상 메서드: 반드시 하위 클래스가 오버라이딩 해야 한다.

템플릿 메서드에서 호출하는 훅 메서드: 하위 클래스가 선택적으로 오버라이딩한다.

 

팩터리 메서드 패턴 (Factory Method Pattern)

오버라이드된 메서드가 객체를 반환하는 패턴

팩터리 메서드는 객체를 생성/반환하는 메서드를 말한다.

팩터리 메서드 패턴은 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하는 것을 의미한다.

// 추상 팩터리 메서드
public abstract class Animal {

    // 추상 팩터리 메서드
    abstract AnimalToy getToy();
}

// 추상 팩터리 메서드 오버라이딩
public class Dog extends Animal {

    @Override
    AnimalToy getToy() {
        return new DogToy();
    }
}

public class Driver {
    public static void main(String[] args) {
        Animal bolt = new Dog();
        Animal kitty = new Cat();
        
        AnimalToy boltBall = bolt.getToy();
        AnimalToy kittyTower = kitty.getToy();
        
        boltBall.identify();
        kittyTower.identify();
    }
}

전략 패턴 (Strategy Pattern)

클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴

전략 패턴 구성요소 3가지

  • 전략 메서드를 가진 전략 객체
  • 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
  • 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트 (전략 객체의 공급자)
// 전략 인터페이스
public interface Strategy {
    public abstract void runStrategy();
}

// 전략 인터페이스 구현
public class StrategyGun implements Strategy {
    
    @Override
    public void runStrategy() {
        System.out.println("탕, 타당, 타다당");
    }
}
public class StrategySword implements Strategy {
    
    @Override
    public void runStrategy() {
        System.out.println("챙! 챙챙! 챙챙챙!");
    }
}

// 전략을 사용하는 컨텍스트
public class Soldier {
    void runContext(Strategy strategy) {
        System.out.println("전투 시작!");
        strategy.runStrategy();
        System.out.println("전투 종료!");
    }
}

// 전략 패턴의 클라이언트
public class Client {
    public static void main(String[] args) {
        Strategy strategy = null;
        Soldier rambo = new Soldier();
        
        strategy = new StrategyGun();
        rambo.runContext(strategy);
        
        strategy = new StrategySword();
        rambo.runContext(strategy);
                
    }
}

클라이언트는 전략을 다양하게 변경하면서 컨텍스트를 실행할 수 있다.

 

전략 패턴에는 OCP, DIP가 적용된다.

 

템플릿 콜백 패턴 (Template Callback Pattern)

전략을 익명 내부 클래스로 구현한 전략 패턴

템플릿 콜백 패턴은 전략 패턴의 변형으로 스프링 3대 프로그래밍 모델 중 하나인 DI에서 사용하는 특별한 형태의 전략 패턴이다.

전략 패턴과 동일하지만 전략을 익명 내부 클래스로 정의해서 사용한다.

 

public class Client {
	public static void main(String[] args) {
		Soldier rambo = new Soldier();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("총! 총초종총 총! 총!");
			}
		});

		System.out.println();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("칼! 카가갈 칼! 칼!");
			}
		});

		System.out.println();

		rambo.runContext(new Strategy() {
			@Override
			public void runStrategy() {
				System.out.println("도끼! 독독..도도독 독끼!");
			}
		});
	}
}

 

위 코드는 중복이 있으므로 리팩토링이 가능하다.

public class Soldier {

    void runContext(String weaponSound) {
        System.out.println("전투 시작");
        executeWeapon(weaponSound).runStrategy();
        System.out.println("전투 종료");
    }
    
    private Strategy executeWeapon(final String weaponSound) {
        return new Strategy() {
            @Override
            public void runStrategy() {
                System.out.println(weaponSound);
            }
        };
    }
}

public class Client {
    public static void main(String[] args) {
        Soldier rambo = new Soldier();
        
        rambo.runContext("총! 총초종총 총! 총!");
        System.out.println();
        
        rambo.runContext("칼! 카가갈 칼! 칼!");
        System.out.println();
        
        rambo.runContext("도끼! 독독..도도독 독끼!");
    }
}

중복되는 전략을 생성하는 코드가 컨텍스트 내부로 들어왔다. (중복되는 부분을 컨텍스트로 이관)

 

스프링은 이런식으로 리팩터링된 템플릿 콜백 패턴을 DI에 적극 활용하고 있다.

OCP, DIP 적용된 설계 패턴이다.

 

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !