독서

[디자인 패턴의 아름다움] ch 2.4 ~ 2.9

오렌지색 귤 2025. 3. 16. 21:41
반응형

2.4 ~ 2.9장

주제 : 객체지향 프로그래밍의 특징

 

추상화 관련

p. 61

함수를 사용할 때 우리는 함수가 어떻게 구현되는지가 아니라 어떤 함수가 있는지를 이해하면 충분하다.

 

p. 63

함수형 프로그래밍의 함수는 프로그래밍 언어에서 말하는 함수가 아니라 y = f(x) 같은 수학의 함수 또는 표현식을 의미한다.

 

Java 의 함수 인터페이스

 

p. 65

Java는 함수형 프로그래밍을 위해 세 가지 새로운 문법 개념인 Stream 클래스, 람다 표현식, 함수형 인터페이스를 도입했다.

 

p. 68

C 언어의 경우 함수를 변수로서 사용할 수 있는 함수 포인터를 지원하지만, Java는 함수 포인터를 지원하지 않기 때문에 대신 함수형 인터페이스를 통해 함수를 감싼 것을 변수로 사용한다.

 

 

파싱 메서드의 최적 위치

1. 엔티티 내부에 두는 경우

 

장점

1. 데이터 구조와 파싱 로직이 함께 있어 응집도가 높음

2. 엔티티 구조가 변경되면 파싱 로직도 함께 변경되므로 유지보수가 용이

 

단점

1. 엔티티가 표현 계층의 관심사(JSON, XML 파싱 등)를 갖게 됨

2. 다양한 파싱 로직이 필요할 경우 엔티티가 비대해짐

3. JPA 엔티티의 경우 영속성 계층 외의 의존성을 가지면 테스트와 유지보수가 어려워짐

 

2. BO (비지니스 객체) 내부에 두는 경우

 

장점

1. 비즈니스 로직의 일부로서 파싱 로직을 관리할 수 있음

2. 엔티티보다 유연하게 파싱 관련 의존성을 가질 수 있음

 

단점

1. BO가 표현 계층의 관심사를 갖게 됨

2. 다양한 표현 형식을 처리해야 할 경우 BO가 비대해질 수 있음

 

3. 서비스 내부의 private 메서드로 두는 경우

 

장점

1. 특정 서비스에서만 사용되는 파싱 로직이라면 적절함

2. 비즈니스 로직 흐름 내에서 파싱 과정을 명확히 제어할 수 있음

 

단점

1. 여러 서비스에서 동일한 파싱 로직이 필요하면 중복 발생

2. 서비스 클래스가 비대해질 수 있음

3. 파싱 로직의 테스트가 어려울 수 있음

 

4. 별도의 매퍼 클래스를 만드는 경우

 

장점

1. 단일 책임 원칙을 잘 준수함

2. 다양한 파싱/변환 로직을 한 곳에서 관리할 수 있음

3. 재사용성이 높음

4. 테스트하기 용이함

 

단점

1. 추가적인 클래스로 인해 프로젝트 복잡도가 증가할 수 있음

2. 매퍼와 도메인 객체 간의 결합도 관리가 필요함

 

권장하는 방법

 

일반적으로 별도의 매퍼 클래스를 만드는 것이 가장 좋은 접근법입니다. 이는 단일 책임 원칙을 잘 준수하며, 파싱/변환 책임을 명확히 분리합니다. 특히 MapStruct나 ModelMapper 같은 매핑 라이브러리를 활용하면 더욱 효과적입니다.

 

 

여러 클래스에서 사용되는 상수의 선언 위치

1. 도메인 관련 상수 : 해당 도메인 클래스(엔티티/BO) 내에 static final로 선언

2. 여러 도메인에 걸친 비지니스 상수 : 의미 있는 Enum으로 만들기

3. 기술적/인프라 상수 : 관련 구성 클래스(Constants)나 유틸리티에 모으기

4. 애플리케이션 전체 설정 상수 : 프로퍼티 파일로 외부화하고 @Value나 @ConfigurationProperties로 주입

 

 

 

Deep Dive (자바의 함수형 인터페이스)

1. Spring Framework 의 TransactionCallback

@FunctionalInterface
public interface TransactionCallback<T> {
    T doInTransaction(TransactionStatus status);
}

 

실제 사용 예시 (Spring Framework 의 TransactionTemplate 클래스)

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    // 트랜잭션 시작
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    
    try {
        // 콜백 실행
        result = action.doInTransaction(status);
    }
    catch (RuntimeException | Error ex) {
        // 롤백 처리
        rollbackOnException(status, ex);
        throw ex;
    }
    
    this.transactionManager.commit(status);
    return result;
}

// 사용 예시
Integer count = transactionTemplate.execute(status -> {
    return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
});

 

실행 흐름

1. transactionTemplate.execute(...)를 호출합니다.

2. execute 메서드 내부에서 transactionManager.getTransaction(this)를 통해 새 트랜잭션을 시작하고 TransactionStatus 객체를 생성합니다.

3. try 블록 내에서 action.doInTransaction(status)를 호출합니다. 이때:

  • action은 전달된 람다 표현식 (status -> { ... })입니다.
  • doInTransaction 메서드는 람다 표현식의 본문을 실행합니다.
  • 람다 표현식 내부의 코드인 jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class)가 실행됩니다.

4. SQL 쿼리가 실행되고 결과가 반환됩니다.

5. 람다 표현식의 결과값이 result에 저장됩니다.

6. 예외가 발생하지 않았다면 transactionManager.commit(status)를 호출하여 트랜잭션을 커밋합니다.

7. result 값(여기서는 사용자 수)을 반환합니다.

 

사용 이유와 장점

 

 

  • 템플릿 메서드 패턴의 간소화: 전통적인 템플릿 메서드 패턴은 클래스 상속이나 인터페이스 구현을 필요로 했지만, 함수형 인터페이스를 사용하면 람다 표현식으로 간단히 처리할 수 있습니다.
  • 보일러플레이트 코드 감소: 트랜잭션의 시작, 커밋, 롤백과 같은 공통 코드를 템플릿에 두고, 비즈니스 로직만 람다로 전달합니다.
  • 명확한 관심사 분리: 트랜잭션 관리 코드와 비즈니스 로직이 명확하게 분리되어 코드의 가독성과 유지보수성이 향상됩니다.

 

 

2. Java Stream API 의 Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    
    // 디폴트 메서드와 정적 메서드는 여러 개 가질 수 있음
    default Predicate<T> and(Predicate<? super T> other) { ... }
    default Predicate<T> or(Predicate<? super T> other) { ... }
    default Predicate<T> negate() { ... }
    static <T> Predicate<T> isEqual(Object targetRef) { ... }
}

 

실제 사용 예시 (Apache Commons Collections 의 PredicateUtils 클래스)

// Commons Collections의 코드
public static <T> Predicate<T> allPredicate(final Predicate<? super T>... predicates) {
    return t -> {
        for (final Predicate<? super T> predicate : predicates) {
            if (!predicate.test(t)) {
                return false;
            }
        }
        return true;
    };
}

// 사용 예시
List<String> filtered = list.stream()
    .filter(Predicate.isEqual("target"))
    .collect(Collectors.toList());

 

사용 이유와 장점

 

  • 조합 가능성(Composability): and(), or(), negate() 등의 디폴트 메서드를 통해 조건을 조합할 수 있어 복잡한 필터링 로직을 선언적으로 표현할 수 있습니다.
  • 재사용성: 자주 사용되는 조건을 Predicate로 정의하여 여러 곳에서 재사용할 수 있습니다.
  • 코드의 의도 명확화: 익명 클래스 대신 람다를 사용하여 "무엇을 할 것인가"에 초점을 맞춘 코드를 작성할 수 있습니다.

 

 

3. Spring Data JPA 의 Specification

@FunctionalInterface
public interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
    
    default Specification<T> and(Specification<T> other) { ... }
    default Specification<T> or(Specification<T> other) { ... }
    default Specification<T> not() { ... }
}

 

실제 사용 예시 (Spring Data JPA 프로젝트)

public class UserSpecifications {
    public static Specification<User> hasAge(int age) {
        return (root, query, cb) -> cb.equal(root.get("age"), age);
    }
    
    public static Specification<User> hasName(String name) {
        return (root, query, cb) -> cb.like(root.get("name"), "%" + name + "%");
    }
}

// 사용 예시
List<User> users = userRepository.findAll(
    where(hasAge(30)).and(hasName("John"))
);

 

사용 이유와 장점

 

  • 복잡한 쿼리의 모듈화: 복잡한 데이터베이스 쿼리 조건을 작은 단위로 모듈화하여 조합할 수 있습니다.
  • 동적 쿼리 생성 용이: 조건에 따라 쿼리를 동적으로 구성하기 쉽습니다.
  • 타입 안전성: Criteria API를 사용하면서도 타입 안전성을 보장받을 수 있습니다.
  • DSL(Domain Specific Language) 구축: 비즈니스 도메인에 특화된 쿼리 언어를 만들 수 있습니다.

 

 

4. CompletableFuture 의 콜백 메서드

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 

실제 사용 예시 (Vert.x 의 Future 구현)

// Vert.x의 Future 사용 예시
Future<String> future = vertx.createDnsClient().lookup("vertx.io")
    .map(ip -> {  // Function 사용
        return "IP: " + ip;
    })
    .onSuccess(result -> {  // Consumer 사용
        System.out.println("Success: " + result);
    })
    .onFailure(err -> {  // Consumer 사용
        System.err.println("Error: " + err.getMessage());
    });

 

사용 이유와 장점

 

  • 비동기 코드의 가독성 향상: 콜백 지옥(Callback Hell)을 피하고 비동기 코드를 순차적으로 읽을 수 있게 합니다.
  • 조합 가능한 비동기 연산: 여러 비동기 작업을 연결하여 복잡한 비동기 흐름을 구성할 수 있습니다.
  • 에러 처리 간소화: try-catch 대신 onFailure와 같은 메서드로 에러를 처리할 수 있습니다.
  • 비동기 코드의 단위 테스트 용이: 콜백을 함수형 인터페이스로 분리하면 테스트하기 쉬워집니다.

 

 

5. Spring Security 의 AuthorizationManager

@FunctionalInterface
public interface AuthorizationManager<T> {
    AuthorizationDecision check(Supplier<Authentication> authentication, T object);
    
    default AuthorizationManager<T> and(AuthorizationManager<T> other) { ... }
    default AuthorizationManager<T> or(AuthorizationManager<T> other) { ... }
}

 

실제 사용 예시 (Spring Security 코드)

// Spring Security의 HTTP 보안 설정
http.authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/user/**").hasRole("USER")
    .anyRequest().authenticated()
);

// 내부적으로는 다음과 같이 구현됨
AuthorizationManager<RequestAuthorizationContext> hasRole = (auth, context) -> {
    return new AuthorizationDecision(auth.get().getAuthorities().contains(new SimpleGrantedAuthority("ROLE_" + role)));
};

 

사용 이유와 장점

 

  • 선언적 보안 정책: 보안 규칙을 선언적인 방식으로 표현할 수 있습니다.
  • 조합 가능한 인가 규칙: and(), or() 메서드를 통해 복잡한 인가 규칙을 구성할 수 있습니다.
  • 커스텀 보안 로직 통합 용이: 기존 인가 로직에 커스텀 로직을 쉽게 추가할 수 있습니다.
  • 테스트 용이성: 보안 로직을 독립적으로 테스트하기 쉽습니다.

 

 

 

얘기 나누고 싶은 내용

 

1. MapStruct나 ModelMapper 사용 빈도

2. Entity, BO, VO의 코드 중복 문제를 처리하는 방법에는 어떤 것이 있을까?

반응형