“상속은 나쁘다”, “합성이 항상 우월하다”는 말을 자주 듣는다. 하지만 실무에서는 이런 이분법적 사고가 오히려 비효율적일 수 있다. 이번 글에서는 상속(Inheritance)과 합성(Composition)을 실무에서 어떻게 선택하고 활용하는지, 실제 경험을 바탕으로 정리해보겠다.
핵심 질문: 상속은 정말 나쁜 습관일까?
객체지향 설계에서 코드 재사용을 위해 상속을 쓰는 것이 나쁜 습관일까? 합성이 항상 우월할까?
결론부터 말하자면: “상황에 따라 다르다”이다.
상속과 합성은 각각 적합한 장소에서 사용해야 하는 코드 설계 원칙이다. 이 둘을 적절히 활용하는 것이 실무에서 더 중요하다.
상속을 사용하는 실무 시나리오
1. 언제 상속을 고려하는가?
실무에서는 처음부터 상속을 설계하지 않는다. 대신 다음과 같은 접근을 한다:
- 먼저 코드를 작성한다
- 중복 패턴이 발견되면 그때 판단한다
- 상속으로 올릴지 말지 결정한다
여기서 「상속으로 올린다」는 여러 클래스에 똑같이 들어 있는 필드나 메서드를 부모 클래스 하나로 옮기고, 원래 클래스들이 그 부모를 extends 하게 바꾸는 일을 말한다. 아래 DTO 예제가 바로 그 결과다.
이런 “선 개발, 후 리팩토링” 방식이 실무적으로 더 합리적이다.
2. DTO에서의 상속 활용
상속이 가장 효과적으로 사용되는 곳은 바로 DTO(Data Transfer Object)이다.
공통 필드 추출 예제
// ✅ 상속으로 공통 필드 관리
public class BaseDto {
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private String createdBy;
// 공통 메소드
public void setAuditInfo(String userId) {
this.createdBy = userId;
this.createdAt = LocalDateTime.now();
}
}
// 로그인 요청 DTO
public class LoginRequestDto extends BaseDto {
private String username;
private String password;
}
// 페이징 요청 DTO
public class PageRequestDto extends BaseDto {
private int page;
private int size;
private String sort;
}
한 줄씩 따라가기
private LocalDateTime createdAt;—LocalDateTime은 날짜와 시각(연·월·일·시·분·초)을 담는 자바 클래스다.createdAt은 만든 시각,updatedAt은 고친 시각,createdBy는 만든 사람이다setAuditInfo(String userId)— Audit(감사)은 「누가 언제 이 데이터를 만들었는지」를 남겨 두는 기록을 말한다. 이 메서드는 만든 사람을userId로, 만든 시각을 지금으로 채운다.LocalDateTime.now()는 컴퓨터 시계에서 읽은 지금 날짜와 시각을 돌려준다LoginRequestDto extends BaseDto—BaseDto를 부모로 둔다. 그래서LoginRequestDto는 자기 필드(username,password) 말고도createdAt같은 공통 필드와setAuditInfo를 물려받는다PageRequestDto— 페이징 요청, 즉 「목록을 몇 번째 페이지(page)부터, 한 페이지에 몇 개씩(size), 무엇을 기준으로 정렬해서(sort) 달라」는 요청을 담는 DTO다. 이것도 같은 부모를 물려받는다
왜 DTO에서 상속이 유용한가?
- 데이터 구조가 명확하게 보인다 — 공통 필드는
BaseDto한 곳에만 있고 자식에는 자기만의 필드만 남는다. 그래서 무엇이 모든 DTO에 공통이고 무엇이 그 DTO에만 있는지가 한눈에 갈린다 - 공통 필드를 한 곳에서 관리할 수 있다
- 코드 중복을 효과적으로 줄인다
3. 상속 사용 시 제약 사항
실무에서는 다음과 같은 규칙을 지키는 것이 좋다:
최대 1레벨 상속만 허용
아래 칸은 자바 코드가 아니라 상속 관계를 줄여 그린 것이다. A → B는 「B가 A를 extends 한다」는 뜻이다.
// ❌ 다단계 상속은 복잡도를 증가시킴
BaseDto → CommonDto → UserDto → AdminUserDto (X)
// ✅ 1레벨 상속으로 단순하게 유지
BaseDto → UserDto (O)
단계가 늘면 무엇이 복잡해지는지는 맨 아래 AdminUserDto를 열어 보면 보인다. 이 클래스에 어떤 필드와 메서드가 있는지 알려면 UserDto → CommonDto → BaseDto로 세 번 올라가며 읽어야 한다. 그리고 중간의 CommonDto 하나를 고치면 그 아래 UserDto와 AdminUserDto가 함께 바뀐다. 1레벨이면 부모 하나만 보면 된다.
불필요한 필드나 메소드를 상속받지 않도록 설계
- 자식 클래스가 부모의 일부만 필요하다면, 그건 잘못된 상속이다 — 상속은 부모의 필드와 메서드를 골라 받을 수 없고 전부 받는다. 그러면 자식에는 쓰지도 않는 필드와 메서드가 생기고, 다른 코드가 그 메서드를 불러도 막을 수 없다
- 이런 경우 공통 부분을 부모 클래스로 옮기지(상속으로 올리지) 말아야 한다
합성(Composition)이 필수인 경우
1. Spring Boot의 기본 패턴
Spring Boot를 사용하면 대부분 합성을 사용하게 된다. 왜냐하면 의존성 주입(DI) 자체가 합성 패턴이기 때문이다. 의존성 주입이란 내가 필요한 객체를 직접 new로 만드는 대신, Spring이 미리 만들어 둔 객체를 찾아서 필드나 생성자에 대신 넣어 주는 것을 말한다 — “이 객체 안에 저 객체를 넣는다”는 모양 자체가 상속이 아니라 합성이다.
그림의 상자를 하나씩 보면 이렇다 — Controller는 바깥에서 들어오는 요청을 제일 먼저 받는 창구이고(자세한 동작은 아래에서 코드와 함께 다시 설명한다), UserService·OrderService·PaymentService 같은 Service는 그 요청을 실제로 처리하는 담당자이며, Repository(UserRepository·OrderRepository)는 DB에서 데이터를 읽고 쓰는 창구다. 화살표의 “의존성 주입”은 위 상자가 아래 상자를 필드로 들고 있다는 뜻이다 — Controller가 Service를, Service가 Repository를 들고 있다.
2. 왜 Service/Repository는 합성을 사용할까?
Java의 단일 상속 제약 때문이다. 상속을 하면 부모의 메서드를 내 메서드처럼 부를 수 있다. 그래서 「Controller에서 UserService의 메서드를 쓰고 싶다」면 UserService를 상속하는 방법을 떠올릴 수도 있다. 그런데 Controller는 여러 Service를 사용해야 하고, 자바에서 extends 뒤에는 클래스를 1개만 쓸 수 있다.
// ❌ 상속으로는 불가능 (자바는 다중 상속 불가)
public class OrderController extends UserService, OrderService, PaymentService {
// 컴파일 에러!
}
// ✅ 합성으로 여러 의존성 조합
@RestController
@RequiredArgsConstructor
public class OrderController {
private final UserService userService;
private final OrderService orderService;
private final PaymentService paymentService;
@PostMapping("/orders")
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// 여러 Service를 자유롭게 조합하여 사용
User user = userService.findById(request.getUserId());
Order order = orderService.create(request);
paymentService.process(order);
return new OrderResponse(order);
}
}
한 줄씩 따라가기
@RestController— 이 클래스가 Controller라는 표시다. 밖에서 온 HTTP 요청을 제일 먼저 받고, 메서드가 돌려주는 값을 그대로 응답 본문(JSON)으로 바꿔 준다@RequiredArgsConstructor— Lombok이 주는 표시로,final로 선언한 필드(userService,orderService,paymentService)만 받는 생성자를 자동으로 만들어 준다. 손으로 생성자를 적지 않아도 되고, Spring은 그렇게 자동으로 생긴 생성자를 보고 세 Service를 찾아서 넣어 준다@PostMapping("/orders")—/orders주소로 들어오는 POST 요청(HTTP 메서드 가운데 「새로 만들기」를 뜻하는 것)을 이 메서드가 처리한다는 뜻이다@RequestBody OrderRequest request— 요청 본문의 JSON을OrderRequest객체로 바꿔서 매개변수로 받는다
3. Spring의 생성자 주입 패턴
@Component
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired // Spring 4.3부터는 단일 생성자인 경우 생략 가능
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
}
한 줄씩 따라가기
CustomerPreferenceDao— 이름 끝의 Dao는 DB를 읽고 쓰는 일을 맡은 객체라는 뜻이다.MovieRecommender는 이 객체를 상속하지 않고 필드로 받아 둔다@Component— 이 클래스를 Spring이 관리하는 객체(빈)로 등록하라는 표시다. 이 표시가 있어야 다른 클래스가 이 객체를 주입받을 수 있다@Autowired— 생성자에 필요한 객체를 Spring이 찾아서 넣어 달라는 표시이다. 주석에 적힌 대로 생성자가 하나뿐이면 이 표시 없이도 Spring이 알아서 넣어 준다
이것이 바로 합성(Composition) 패턴이다. 객체를 상속받는 것이 아니라, 필요한 객체를 멤버 변수로 포함하는 방식이다.
DTO에서 합성 vs 상속
1. DTO에서 합성이 복잡해지는 이유
DTO에서 외부 객체와 합성을 하면 의존성이 “엉킨 실처럼 복잡해진다”:
// ❌ DTO가 외부 객체에 의존 (비추천)
public class UserResponse {
private String username;
private OrderService orderService; // DTO가 Service를 의존?
private ExternalOrderInfo orderInfo; // 외부 클래스 의존
// 복잡도 증가, 테스트 어려움, DTO의 역할 모호
}
DTO는 Controller가 응답으로 내보낼 값을 담아 나르는 상자다. 코드 주석의 「의존한다」는 여기서 그 객체를 필드로 들고 있어서, 그것 없이는 이 클래스가 만들어지거나 동작할 수 없다는 뜻이다. 그런데 이 상자가 OrderService를 필드로 들면 무엇이 엉키는지 따라가 보면 이렇다.
UserResponse하나를 만들려면OrderService가 있어야 한다.OrderService는 또OrderRepository를 들고 있고, 그 뒤에는 DB가 있다. 값만 담으면 되는 상자에 Service · Repository · DB가 줄줄이 따라붙는다- 그래서 테스트도 어렵다.
UserResponse에 값이 제대로 담기는지만 보고 싶어도,OrderService와 그 뒤의 것들을 준비하거나 가짜(Mock)로 만들어 넣어야 한다 ExternalOrderInfo의 「외부 클래스」는 이 DTO 바깥에서 정의되어, 그쪽 사정으로 모양이 바뀔 수 있는 클래스를 가리킨다. 그 클래스가 바뀌면 이 DTO도 같이 고쳐야 한다
2. Inner Class로 응집도 유지
대신 Inner Class를 활용하여 DTO 내부에서 처리하는 것이 좋다. 제목의 응집도는 함께 쓰이는 것끼리 한곳에 모여 있는 정도다 — 주문 정보를 담는 모양을 UserResponse 안에 두면, 응답 모양을 고칠 때 이 파일 하나만 열면 된다. Inner Class는 클래스 안에 또 다른 클래스를 선언하는 자바 문법이다. 아래 예제의 OrderInfo처럼 static class로 선언하면, UserResponse 밖에 별도 파일을 만들지 않고도 UserResponse.OrderInfo라는 이름으로 접근할 수 있는 클래스가 생긴다. static을 빼면 OrderInfo는 바깥 UserResponse 객체가 먼저 있어야만 만들 수 있는 클래스가 된다(static 메서드 안에서 new UserResponse.OrderInfo()처럼 바로 만들면 컴파일 에러가 난다). OrderInfo는 바깥 객체 없이 혼자 만들어져야 하니 static을 붙인다.
앞의 나쁜 예와 달라지는 점이 하나 더 있다. 나쁜 예는 OrderService를 들고 있다가 DTO 안에서 주문을 찾으려 했다. 아래 예는 주문을 찾는 일을 DTO가 하지 않는다. DTO를 만드는 쪽이 이미 꺼내 온 User 객체를 생성자로 넘기고, DTO는 그 안의 주문 목록(user.getOrders())을 옮겨 담기만 한다:
// ✅ Inner class로 응집도 유지
@Getter
public class UserResponse {
private String username;
private String email;
private List<OrderInfo> orders;
@Getter
public static class OrderInfo {
private String orderId;
private BigDecimal amount;
private LocalDateTime orderDate;
public OrderInfo(Order order) {
this.orderId = order.getId();
this.amount = order.getAmount();
this.orderDate = order.getCreatedAt();
}
}
public UserResponse(User user) {
this.username = user.getUsername();
this.email = user.getEmail();
this.orders = user.getOrders().stream()
.map(OrderInfo::new)
.collect(Collectors.toList());
}
}
한 줄씩 따라가기
@Getter(클래스에 붙은 것) — Lombok 표시로,UserResponse의 모든 필드에 대해getUsername()처럼 get으로 시작하는 조회 메서드를 자동으로 만들어 준다.OrderInfo에 붙은@Getter도 마찬가지로 그 클래스의 필드에 적용된다private BigDecimal amount;— 금액은double대신BigDecimal로 담는다.double은 소수 계산에서 작은 오차가 생겨서, 한 자리도 틀리면 안 되는 돈에는 맞지 않는다user.getOrders().stream()—User가 가진 주문 목록(List<Order>)을 스트림으로 바꾼다. 스트림으로 바꾸면 반복문 없이 “하나씩 변환한다”, “조건에 맞는 것만 거른다” 같은 동작을 이어 붙여 적을 수 있다.map(OrderInfo::new)— 스트림에 있는Order객체 하나하나를new OrderInfo(그 Order)로 바꾼다.OrderInfo::new는order -> new OrderInfo(order)를 줄여 쓴 메서드 참조 문법이다.collect(Collectors.toList())— 변환된OrderInfo들을 다시List<OrderInfo>로 모은다. 이 결과가this.orders에 담긴다
장점:
- DTO 내부에서 모든 것이 해결된다
- 외부 의존성이 없어 테스트가 쉽다 — 생성자가
User와Order를 받기는 하지만, Service나 Repository처럼 DB까지 이어지는 객체는 들지 않는다는 뜻이다. 테스트에서는User객체 하나를 만들어 생성자에 넘기면 끝나고, Spring을 띄우거나(스프링 컨텍스트를 부팅해 빈을 전부 만드는 일) Service를 준비할 필요가 없다 - 데이터 구조가 명확하게 보인다
그림의 AddressInfo Inner Class는 위 코드에는 없다. OrderInfo처럼 “UserResponse 안에 주소 정보만 담는 또 다른 Inner Class를 똑같은 방식으로 만들 수 있다”는 걸 보여 주려고 그림에만 더해 둔 예시이고, 실제로 구현한 것은 OrderInfo 하나뿐이다.
실무 판단 기준 정리
상속을 사용하는 경우
| 상황 | 판단 기준 | 예시 |
|---|---|---|
| DTO 공통 필드 | 여러 DTO에서 동일한 필드가 반복될 때 | BaseDto (createdAt, updatedAt) |
| 공통 메소드 | 공통 필드를 사용하는 메소드가 있을 때 | 페이징 정보, 감사(Audit) 정보 |
| 상속 깊이 | 최대 1레벨까지만 | BaseDto → UserDto (O) |
표의 「페이징 정보」는 위 예제와 모양이 다르다. 위 예제에서 page·size·sort는 PageRequestDto 하나에만 있는 고유 필드였다. 페이징 필드를 여러 DTO가 똑같이 가질 때에야 공통 메소드의 예가 된다. 위 예제에서 실제로 공통 메소드였던 것은 감사 정보를 채우는 setAuditInfo다.
합성을 사용하는 경우
| 상황 | 판단 기준 | 예시 |
|---|---|---|
| 여러 기능 조합 | 여러 객체를 함께 사용해야 할 때 | Controller → 여러 Service |
| 의존성 주입 | Spring DI 패턴 | @Autowired, 생성자 주입 |
| 동적 변경 | 런타임에 구현체를 바꿔야 할 때 | 인터페이스 기반 설계 |
「동적 변경」 줄은 이렇게 읽는다. 상속은 extends 부모 로 부모를 코드에 적어 두므로, 컴파일할 때 부모가 정해지고 실행 중에는 바꿀 수 없다. 합성은 필드 타입을 인터페이스로 두고 그 자리에 어떤 구현체 객체를 넣을지를 생성자로 받는다. 그래서 같은 클래스라도 실행할 때 넘겨받은 객체에 따라 다른 구현체로 동작한다. 아래 「실전 코드 비교」에서 ServiceLogger를 생성자로 받아 테스트 때 가짜로 바꿔 끼우는 것도 같은 생각이다. 다만 그 예제의 ServiceLogger는 인터페이스가 아니라 클래스다.
Java 공식 문서의 상속과 합성
상속의 장단점
장점:
단점:
- 상속 계층이 깊어지면 복잡도 증가
- 강한 결합도 (부모-자식 간 강한 의존성) — 부모를 고치면 물려받은 자식이 전부 같이 영향을 받는다
Java의 상속 제약
Java는 단일 상속만 지원한다:
// ❌ 다중 상속 불가
public class AdminUser extends User, Employee {
// 컴파일 에러!
}
// ✅ 인터페이스를 통한 다중 구현
public class AdminUser extends User implements Employee, Auditable {
// 이것은 가능 (클래스 1개 + 인터페이스 여러 개)
}
두 예제의 Employee는 서로 다른 선언이라고 보고 읽는다. 첫 예제에서는 Employee가 클래스라서 User와 함께 extends 할 수 없다. 두 번째 예제는 Employee를 클래스가 아니라 인터페이스로 바꿔 선언했다고 가정한 것이다. 인터페이스는 implements 뒤에 여러 개를 쓸 수 있으므로, 클래스 1개(User)를 상속하면서 Employee와 Auditable을 함께 구현할 수 있다.
실전 코드 비교
시나리오: 사용자 관리 시스템
잘못된 상속 사용
// ❌ 상속 남용
public class BaseService {
protected Logger logger = LoggerFactory.getLogger(getClass());
protected String serviceName;
public void logInfo(String message) {
logger.info("[{}] {}", serviceName, message);
}
}
public class UserService extends BaseService {
private UserRepository userRepository;
public User findById(Long id) {
logInfo("Finding user: " + id);
return userRepository.findById(id).orElseThrow();
}
}
public class OrderService extends BaseService {
private OrderRepository orderRepository;
public Order create(OrderRequest request) {
logInfo("Creating order");
return orderRepository.save(new Order(request));
}
}
여기서 쓴 Logger·LoggerFactory는 프로그램이 도는 동안 무슨 일이 있었는지 콘솔에 기록을 남기는 도구다. getLogger(getClass())로 이 클래스 전용 기록기를 하나 만들고, logger.info(...)로 기록을 남긴다. logger.info("[{}] {}", serviceName, message)처럼 메시지 문자열 안에 {}를 넣으면, 그 뒤에 오는 인자들이 나온 순서대로 그 자리를 채운다 — 첫 번째 {}는 serviceName으로, 두 번째 {}는 message로 바뀌어, serviceName에 "UserService"가 들어 있다면 [UserService] Finding user: 1처럼 출력된다. 이 예제에는 serviceName에 값을 넣는 코드도, userRepository를 채우는 생성자도 없다. 상속의 문제만 보이려고 줄인 코드라서, 그대로 실행하는 코드로 읽지 않는다.
userRepository.findById(id).orElseThrow()도 함께 보면 — findById는 찾는 id의 사용자가 없을 수도 있어서 User를 바로 돌려주지 않고 Optional이라는 상자에 담아 돌려준다. orElseThrow()는 그 상자 안에 값이 있으면 꺼내 주고, 비어 있으면 그 자리에서 예외를 던진다.
orderRepository.save(new Order(request))는 새 Order를 DB에 저장하고, 저장한 객체를 돌려준다. Repository를 만들어 주는 Spring Data의 CrudRepository 문서도 save가 저장한 객체를 돌려준다고 적고, 「저장 작업이 객체를 완전히 바꿔 놓았을 수 있으니 이후에는 돌려받은 객체를 쓰라」고 한다. 무엇이 어떻게 바뀌는지는 이 글에서 다루지 않는다. 그래서 그 결과를 바로 return 한다.
protected는 같은 패키지나 자식 클래스에서 쓸 수 있게 여는 접근 제어자다. getClass()는 지금 실행 중인 객체의 실제 클래스를 돌려준다. BaseService 안에 적혀 있어도, 실제 객체가 UserService라면 UserService 클래스를 돌려준다.
문제점:
- 로깅 기능 때문에 모든 Service가 상속받아야 함
- 나중에 다른 부모 클래스가 필요하면? (단일 상속 제약)
- Service 간의 불필요한 결합도 증가 —
UserService와OrderService는 서로 상관없는 일을 하는데, 같은 부모BaseService에 함께 묶였다. 상속은 부모의 필드와 메서드를 전부 자식의 일부로 만든다. 그래서 주문 쪽 사정으로BaseService에 필드나 메서드를 하나 더 넣으면, 그것이 사용자 쪽UserService안에도 그대로 생긴다. 아래 합성 예제도ServiceLogger.logInfo를 고치면 두 Service가 함께 영향을 받는 것은 같다. 다른 점은 범위다 — 합성에서 두 Service가 기대는 것은ServiceLogger의logInfo메서드 하나뿐이고,ServiceLogger에 무엇을 더 넣어도 Service 클래스 안에 생기지 않는다
올바른 합성 사용
// ✅ 합성으로 개선
@Component
public class ServiceLogger {
private final Logger logger = LoggerFactory.getLogger(ServiceLogger.class);
public void logInfo(String serviceName, String message) {
logger.info("[{}] {}", serviceName, message);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final ServiceLogger serviceLogger;
public User findById(Long id) {
serviceLogger.logInfo("UserService", "Finding user: " + id);
return userRepository.findById(id).orElseThrow();
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ServiceLogger serviceLogger;
public Order create(OrderRequest request) {
serviceLogger.logInfo("OrderService", "Creating order");
return orderRepository.save(new Order(request));
}
}
한 줄씩 따라가기
@Component(ServiceLogger에 붙은 것) — 앞서 설명한 것과 같다.ServiceLogger를 Spring이 관리하는 객체로 등록한다LoggerFactory.getLogger(ServiceLogger.class)—ServiceLogger.class는 클래스 이름 뒤에.class를 붙여 클래스 그 자체를 값으로 넘기는 클래스 리터럴이다. 앞 예제의getClass()는 실행 중인 객체를 보고 클래스를 알아냈지만, 여기서는ServiceLogger로 고정해서 넘긴다@Service—@Component와 하는 일은 같지만, “이 클래스는 업무 로직을 처리하는 Service 계층이다”라는 역할을 이름으로 드러내는 표시다.UserService와OrderService가 이 표시를 달고 있다@RequiredArgsConstructor와 생성자로serviceLogger를 받는 부분은 앞서OrderController예제에서 설명한 것과 같은 방식이다 — 상속 없이ServiceLogger객체를 필드로 받아 쓰는 합성이다
장점:
- 각 Service가 독립적이다
- ServiceLogger를 쉽게 교체 가능 (테스트 시 Mock) — 합성에서는
ServiceLogger가 생성자로 들어오는 바깥 객체라서, 테스트할 때 진짜 대신 가짜 객체를 생성자에 넘기면 된다.ServiceLogger는 인터페이스가 아니라 클래스지만,ServiceLogger를extends해서logInfo를 아무것도 하지 않게 다시 쓴 테스트용 클래스를 만들면 그 객체가 가짜가 된다. 상속에서는logInfo가UserService자신의 일부(부모에게서 물려받은 메서드)라서, 바깥에서 다른 것으로 갈아 끼울 자리가 없다 - 다른 기능 추가 시 유연하다 — 예를 들어 모든 Service에 알림 기능이 필요해지면, 합성에서는 알림 객체를 필드 하나로 더 받으면 된다. 상속에서는 이미
BaseService를 부모로 쓰고 있어서 알림용 부모를 하나 더 붙일 수 없다
결론: 적재적소에 적합한 도구를 사용하라
핵심 정리
- “상속은 나쁘다”는 교조적 사고를 버린다
- 여기서 “교조적”이란 상황을 따지지 않고 규칙을 무조건 지키려는 태도를 말한다. “상속은 무조건 나쁘다”처럼 예외 없이 믿지 말라는 뜻이다
- 상속과 합성은 각각의 적합한 용도가 있다
- DTO에서는 상속이 효과적이다
- 공통 필드/메소드 관리에 유용
- 단, 1레벨 상속으로 제한한다
- Inner class로 응집도를 유지한다
- 비즈니스 로직에서는 합성을 선호한다
- 비즈니스 로직은 앞의
@Service설명에서 말한 「업무 로직」과 같은 말이다. Service 계층이 처리하는 일이다 - Spring의 DI는 합성 패턴이다
- 여러 기능을 조합할 수 있다
- 테스트가 쉽고 유연하다
- 비즈니스 로직은 앞의
- 선 개발, 후 리팩토링
- 처음부터 완벽한 설계는 어렵다
- 중복이 발견되면 그때 판단한다
실무 체크리스트
다음 질문으로 판단한다:
- 여러 객체를 조합해야 하나? → 합성
- 단순히 공통 데이터를 공유하나? → 상속 고려
- 나중에 다른 부모가 필요할 수 있나? → 합성
- DTO의 공통 필드인가? → 상속
- 테스트를 쉽게 만들고 싶은가? → 합성
자신만의 철학을 만들어가는 중입니다.
댓글남기기