테스트 코드를 먼저 작성하고, 그 테스트를 통과하기 위한 구현 코드를 작성하는 개발 방법론.
코드 작성 전에 테스트를 통해 문제 정의와 요구사항을 명확히 이해.
핵심 개념:
테스트 작성 → 테스트 통과 → 코드 리팩토링.
코드 품질을 높이고, 안정성을 확보.
TDD의 슬로건:
“테스트부터 작성하자. 그리고 통과시키자!”
1.2 TDD의 기본 사이클: Red-Green-Refactor
Red (실패를 즐기자):
실패를 두려워하지 않고, 실패를 통해 요구사항을 이해.
실패한 테스트는 현재 요구사항이 무엇인지를 알려줌.
import org.junit.jupiter.api.Test;
importstatic org.junit.jupiter.api.Assertions.assertEquals;
publicclassCalculatorTest{
@TestvoidtestAddition(){
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result); // 실패! 왜? 구현 코드가 없으니까.
}
}
Green (테스트 통과는 최소 노력으로):
테스트를 통과시키는 코드를 작성.
복잡하게 고민하지 않고, 테스트 요구사항만 충족.
publicclassCalculator{
publicintadd(int a, int b){
return a + b; // 통과!
}
}
Refactor (이제 진짜 멋지게):
코드의 가독성, 유지보수성을 개선.
코드 동작은 동일하게 유지하면서, 구조를 더 깔끔하고 확장 가능하게 변경.
publicclassCalculator{
publicintadd(int a, int b){
return calculate(a, b, "+");
}
privateintcalculate(int a, int b, String operation){
switch (operation) {
case"+":
return a + b;
default:
thrownew UnsupportedOperationException("Operation not supported");
}
}
}
1.3 TDD가 중요한 이유
코드 안정성 보장:
모든 기능에 대해 자동화된 테스트 작성.
버그를 줄이고, 코드 변경에도 안정성 유지.
생산성 향상:
코드 리팩토링 부담 감소.
변경 사항이 발생해도 테스트를 통해 빠르게 문제 발견.
코드 품질 향상:
테스트를 기반으로 설계된 코드는 요구사항에 부합하고, 명확하며 유지보수가 쉬움.
개발 프로세스 개선:
코드 작성 전에 요구사항을 구체화.
의미 없는 코드 작성을 방지.
1.4 TDD의 장단점
장점 단점
코드 신뢰성과 품질 향상.
초기 학습 곡선이 높음.
요구사항 변경에도 코드 구조를 쉽게 유지 가능.
테스트 작성에 시간이 추가적으로 소요.
디버깅 시간 절약 및 문제 사전 예방.
잘못된 테스트 설계 시 오히려 개발 속도 저하.
자동화된 테스트로 코드 변경 시 빠른 피드백 가능.
작은 프로젝트에서는 과도한 테스트 작성 가능성.
1.5 DDD (Domain-Driven Design)와의 관계
TDD:
코드를 테스트하면서 어떻게 구현할지에 초점.
코드 안정성과 품질을 높이기 위해 활용.
DDD:
비즈니스 도메인을 설계하여 무엇을 구현할지에 초점.
요구사항을 분석하고, 도메인 모델을 설계하여 복잡한 비즈니스 로직을 제대로 반영.
TDD와 DDD의 차이:
TDD
DDD
목적
코드의 동작 보장
비즈니스 요구사항을 시스템에 반영
초점
테스트 작성 → 코드 작성 → 리팩토링
요구사항 분석 → 도메인 설계 → 코드 작성
중요성
코드 안정성과 품질 확보
비즈니스 로직의 정확한 반영
1.6 TDD 실습 시 추가 학습이 필요한 주제
테스트 작성 기법:
단위 테스트(Unit Test), 통합 테스트(Integration Test)의 차이와 작성 방법.
Mocking 라이브러리(예: Mockito, MockK) 활용.
테스트 자동화 도구:
CI/CD 파이프라인에서 TDD를 통합하기 위한 Jenkins, GitHub Actions.
테스트 피라미드:
테스트 종류(단위 테스트, 통합 테스트, UI 테스트)와 비중.
테스트 커버리지:
코드의 어느 부분이 테스트되고, 어느 부분이 테스트되지 않는지 분석.
리팩토링 패턴:
코드 리팩토링 시 자주 사용하는 디자인 패턴과 사례.
결론
TDD는 코드 안정성과 품질을 높이는 데 중요한 역할을 하는 개발 방법론입니다. 코드를 작성하기 전에 테스트를 먼저 작성함으로써 요구사항을 명확히 하고, 불필요한 작업을 줄일 수 있습니다. 또한, TDD는 DDD와 함께 사용하면 비즈니스 로직 설계와 구현을 더욱 효율적으로 진행할 수 있습니다.
TDD를 실무에서 성공적으로 적용하기 위해서는 단위 테스트 작성 기법, Mocking, 테스트 피라미드와 같은 추가 개념을 학습하는 것이 중요합니다. 😊
테스트 코드
📌 “내 코드, 정말 제대로 작동할까?”라는 질문에 대한 답을 미리 준비하는 개발자의 무기
프로그램의 기능이 의도대로 동작 하는지 자동으로 확인
안만든다는 선택지도 분명 존재는 하나, 서버를 주력으로 다루는 백엔드에서 오류가 안나도록 테스트를 진행하는 것은 사실상 필수라고 봐야한다.
Mock
1. Mock이란?
Mock(모의 객체):
테스트 환경에서 실제 객체를 대신하여 동작하는 가짜 객체. [실제 객체는 DB,네트워크와 같은 외부 의존성에 의해 움직일 수 있는데, 테스트 시에는 이런게 없으면 편하니 유사 객체를 만든 것]
외부 의존성을 제거하고, 테스트 대상 코드에만 집중할 수 있도록 도와줌.
객체의 특정 동작을 미리 정의하거나 호출 여부를 검증하는 데 사용.
2. Mock의 필요성
외부 의존성 제거:
데이터베이스, 네트워크, 파일 시스템 등 외부 시스템과 상호작용 없이 독립적인 테스트 가능.
테스트 성능 향상:
실제 객체 대신 Mock 객체를 사용하여 빠르고 가벼운 테스트 환경 구현.
예외 및 경계 조건 테스트:
실제 객체가 처리하지 못하는 특정 조건(예: 네트워크 장애, DB 연결 실패)을 Mock으로 시뮬레이션.
상태 검증:
Mock 객체를 통해 특정 메서드가 호출되었는지, 호출 횟수 등을 확인.
3. Mock과 Stub, Spy의 차이
항목 Mock Stub Spy
정의
테스트 중 동작을 시뮬레이션하는 가짜 객체
고정된 응답을 제공하는 가짜 객체
실제 객체를 감싸서 호출을 감시하고 일부 동작을 대체
주요 기능
호출 검증, 동작 시뮬레이션
미리 정의된 값을 반환
호출 여부와 횟수를 확인하며 일부 메서드를 모킹
사용 목적
메서드 호출 여부, 횟수, 전달된 인수 검증
정해진 동작을 반환
실제 객체와 모킹 동작을 혼합
예시
네트워크 요청 여부 검증
특정 메서드가 항상 같은 값을 반환하도록 설정
데이터베이스 객체의 동작 일부를 검증
4. Mock 구현 예제
Java - Mockito 사용
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
importstatic org.mockito.Mockito.*;
classUserServiceTest{
@TestvoidtestGetUserName() {
// Mock 객체 생성
UserRepository mockRepo = mock(UserRepository.class);
// Mock 동작 정의
when(mockRepo.getUserName(1)).thenReturn("Alice");
// Mock 객체를 사용하는 서비스
UserService userService = new UserService(mockRepo);
// 테스트String name = userService.getUserName(1);
assertEquals("Alice", name);
// 호출 여부 검증
verify(mockRepo, times(1)).getUserName(1);
}
}
@SpringBootTest@AutoConfigureMockMvc
class UserServiceIntegrationTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var userRepository: UserRepository
@BeforeEach
fun setUp() {
userRepository.save(User("Alice", "alice@example.com"))
}
@AfterEachfuntearDown() {
userRepository.deleteAll()
}
@Testfun `shouldreturnuserdetails`() {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"))
}
}
6. 통합 테스트 시 Mock과 실제 객체의 사용
Mock 사용:
외부 API나 비효율적인 작업(Mock API, Mock 데이터베이스 등) 테스트.
네트워크 장애, 예외 처리 시뮬레이션.
실제 객체 사용:
데이터베이스와의 CRUD 테스트.
실제 파일 시스템이나 외부 API와의 연동 확인.
7. 통합 테스트 시 주의사항
테스트 환경 설정:
테스트용 데이터베이스와 실제 데이터베이스를 분리.
환경 변수나 설정 파일로 테스트 환경 구성.
테스트 격리:
각 테스트가 독립적으로 실행되도록 설정.
테스트 실행 전후에 데이터를 초기화(Clean-up).
실행 시간 최적화:
불필요한 작업(Mock 사용) 줄이고, 실행 시간을 최소화.
실제 환경과 유사성:
가능한 실제 환경과 비슷한 조건에서 테스트 수행.
8. 통합 테스트와 단위 테스트의 차이
단위 테스트 (Unit Test)
통합 테스트 (Integration Test)
테스트 범위
개별 구성 요소(메서드, 클래스 등).
여러 구성 요소 또는 시스템 전체.
실행 속도
빠름.
느림.
의존성 제거
Mock 객체를 사용해 외부 의존성을 제거.
실제 객체 또는 Mock을 혼합 사용.
사용 목적
개별 구성 요소의 동작을 검증.
구성 요소 간의 상호작용과 통합 검증.
테스트 복잡도
상대적으로 단순.
환경 설정 및 데이터 초기화가 필요하므로 복잡.
9. 추가 학습이 필요한 주제
Mock Server 활용:
외부 API 연동 테스트 시 Mock 서버 구현.
TestContainers:
Docker 컨테이너를 사용해 실제 데이터베이스, 메시지 큐 테스트.
CI/CD 파이프라인 통합:
통합 테스트를 자동화하여 지속적인 배포 환경 구축.
테스트 격리 전략:
데이터 충돌을 방지하기 위한 테스트 데이터 격리 기법.
End-to-End 테스트 (E2E Test)
1. E2E 테스트란?
End-to-End 테스트:
애플리케이션의 전체 플로우를 검증하는 테스트.
사용자가 애플리케이션을 실제로 사용하는 것과 동일한 환경에서 테스트를 진행.
프론트엔드(웹, 모바일) → 백엔드 → 데이터베이스 → 외부 서비스 등 모든 구성 요소의 통합 동작 확인.
2. E2E 테스트의 목적
사용자 관점의 테스트:
사용자가 애플리케이션을 사용하면서 마주칠 수 있는 모든 시나리오를 검증.
시스템 전체의 신뢰성 확인:
프론트엔드, 백엔드, 데이터베이스 등 시스템 구성 요소 간의 통합 상태 확인.
버그 사전 탐지:
단위 및 통합 테스트에서 발견되지 않은 전체 시스템의 결합 문제를 조기에 탐지.
운영 환경과 유사한 테스트:
실제 배포 환경과 동일한 조건에서 테스트를 수행하여 배포 후 발생할 수 있는 문제를 예방.
3. E2E 테스트의 특징
특징
설명
테스트 범위
시스템의 전체 플로우 검증. 사용자와 애플리케이션의 모든 상호작용 확인.
실행 속도
단위 테스트와 통합 테스트보다 느림.
의존성
실제 시스템(데이터베이스, API, 네트워크 등)과의 상호작용 필요.
사용 사례
로그인, 회원가입, 결제 프로세스, 검색 기능 등 전체 플로우를 포함하는 기능 테스트.
자동화 도구 사용
Selenium, Cypress, Puppeteer, Playwright 등을 활용.
4. E2E 테스트의 장단점
장점
단점
실제 사용 시나리오를 기반으로 테스트하므로 사용자 관점에서 신뢰성을 높임.
테스트 속도가 느림.
시스템 전체의 결합 문제를 조기에 발견 가능.
설정 및 유지보수 비용이 높음.
운영 환경과 유사한 조건에서 실행 가능.
외부 의존성(네트워크, API)으로 인해 테스트 불안정성 증가.
주요 사용자 플로우를 검증하므로 배포 후 심각한 결함 발생 가능성을 줄임.
모든 경로를 테스트하려면 과도한 리소스가 필요.
5. E2E 테스트 작성 방법
5.1 주요 단계
테스트 환경 구성:
운영 환경과 동일한 테스트 환경 준비.
데이터베이스, API 서버 등 모든 구성 요소 설정.
사용자 플로우 정의:
사용자가 실제로 수행할 시나리오 정의(예: 로그인 → 상품 검색 → 결제).
자동화 테스트 작성:
Selenium, Cypress 등의 자동화 도구를 사용해 테스트 작성.
테스트 실행 및 결과 검증:
실행 후, 예상 결과와 실제 결과를 비교하여 테스트 통과 여부 확인.
5.2 E2E 테스트 예제Cypress 예제
describe('User Login and Dashboard', () => {
it('should allow the user to log in and view the dashboard', () => {
// Visit the login pagecy.visit('https://example.com/login');
// Enter login credentialscy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('#login-button').click();
// Verify redirection to dashboardcy.url().should('include', '/dashboard');
// Check for welcome messagecy.contains('Welcome, testuser');
});
});
// 나쁜 예if (user != null && user.getAge() > 18 && user.isActive()) {
// do something
}
// 좋은 예if (isActiveAdultUser(user)) {
// do something
}
privatebooleanisActiveAdultUser(User user) {
return user != null && user.getAge() > 18 && user.isActive();
}
조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
isActiveAdultUser가 중요한 것 = 왜 했는지
부정 표현을 긍정 표현으로 바꾸기
// 나쁜 예if (!user.isInActive()) {
return"Inactive User";
}
return"Active User";
// 좋은 예if (user.isActive()) {
return"Active User";
}
return"Inactive User";
긍정적 변수명 사용 - 이거 상당히 중요하다
긍정적 조건문 작성
이중 부정 지양 = 이건 반의어를 생각해봐라, 진짜 웬만해서는 피해야한다.
isinactive 라면 긍정인 isactive로 수정한다. 부정은 결국 한번 더 생각을 해야한다..
else 문 사용 지양하기
// 나쁜 예publicvoidlogin(User user) {
if (user != null) {
if (user.isActive()) {
if (user.isVerified()) {
System.out.println("Login successful");
} else {
System.out.println("User is not verified");
}
} else {
System.out.println("User is inactive");
}
} else {
System.out.println("Invalid user");
}
}
// 좋은 예publicvoidlogin(User user) {
if (user == null) {
System.out.println("Invalid user");
return;
}
if (!user.isActive()) {
System.out.println("User is inactive");
return;
}
if (!user.isVerified()) {
System.out.println("User is not verified");
return;
}
// 모든 조건을 통과한 경우
System.out.println("Login successful");
}
// 더 좋은 예publicvoidlogin(User user) {
String validationResult = validateUser(user);
if (!validationResult.equals("Valid")) {
System.out.println(validationResult);
return;
}
// 모든 조건을 통과한 경우
System.out.println("Login successful");
}
private String validateUser(User user) {
if (user == null) {
return"Invalid user";
}
if (!user.isActive()) {
return"User is inactive";
}
if (!user.isVerified()) {
return"User is not verified";
}
return"Valid";
}
else문을 피하고 기본 동작을 명시
전처리와 핵심 로직을 분리
각 조건은 독립적으로 처리
클린 코드 이론 원칙
클린 코드를 작성하는 데 있어 추가적으로 고려할 수 있는 원칙과 실천 사항은 다음과 같습니다:
1. 코드의 가독성 향상
일관성 유지: 코드 스타일(들여쓰기, 괄호 배치, 공백 등)을 일관되게 유지합니다.
짧은 함수: 함수는 가능한 짧게 유지하여 한눈에 이해할 수 있도록 합니다.
코드 정렬: 논리적 흐름을 고려하여 코드를 정렬합니다(예: 관련된 부분끼리 묶기).
2. 객체 지향 설계 원칙(SOLID)
S - 단일 책임 원칙(Single Responsibility Principle) 클래스나 모듈은 하나의 책임만 가져야 합니다.
O - 개방-폐쇄 원칙(Open/Closed Principle) 코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
L - 리스코프 치환 원칙(Liskov Substitution Principle) 서브클래스는 언제나 기반 클래스의 역할을 대체할 수 있어야 합니다.
I - 인터페이스 분리 원칙(Interface Segregation Principle) 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
D - 의존성 역전 원칙(Dependency Inversion Principle) 상위 모듈은 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
3. 에러 처리
명확한 예외 처리: 예외를 포괄적으로 잡기보다는 특정한 경우에만 잡아야 합니다.
사전 조건 체크: 함수나 메서드의 입력값 검증을 통해 에러를 방지합니다.
null 사용 지양: null 대신 Optional, Maybe 같은 명시적 표현을 사용합니다.
4. 의존성 관리
의존성 주입(DI): 객체 생성과 사용을 분리하여 의존성을 줄입니다.
전역 상태 지양: 전역 변수를 사용하지 않고 명시적으로 데이터를 전달합니다.
5. 성능 및 최적화
불필요한 작업 최소화: 반복 작업이나 연산을 줄이고 캐싱을 활용합니다.
지연 계산(lazy evaluation): 필요할 때만 계산하도록 설계합니다.
효율적인 자료구조 선택: 목적에 맞는 자료구조를 사용하여 성능을 개선합니다.
6. 테스트 가능 코드
단위 테스트 작성: 각 함수와 모듈의 동작을 검증합니다.
테스트 자동화: CI/CD 환경에서 자동화된 테스트를 실행합니다.
모의 객체(Mock Object) 활용: 외부 의존성을 분리하여 테스트를 단순화합니다.
7. 유지보수성을 고려한 설계
코드 중복 제거: 유틸리티 함수나 상수로 공통된 로직을 추출합니다.
확장성 있는 설계: 변경 사항이 최소한의 영향으로 처리되도록 설계합니다.
의도를 드러내는 코드: 코드만 읽어도 설계 의도를 알 수 있게 작성합니다.
8. 클린 아키텍처
계층 분리: 비즈니스 로직, 프레젠테이션, 데이터 접근 계층을 분리합니다.
엔티티 독립성: 엔티티는 프레임워크나 데이터베이스에 의존하지 않도록 설계합니다.
경계(Interface) 설정: 외부 의존성과 내부 로직 사이에 명확한 경계를 만듭니다.
9. 적절한 캡슐화
정보 은닉: 클래스 내부 상태를 외부에서 직접 접근하지 못하도록 합니다.
Getter/Setter 최소화: 단순히 데이터를 노출하는 Getter/Setter보다는 행동 중심의 메서드 사용.
10. 명확한 의사소통
코드 리뷰: 동료와의 코드 리뷰를 통해 더 나은 코드를 작성합니다.
컨벤션 문서화: 팀에서 사용하는 코딩 스타일을 문서로 명시합니다.
개방-폐쇄 원칙 (Open-Closed Principle)
📌 객체 지향 설계의 핵심 원칙 중 하나로, 소프트웨어 모듈, 클래스, 또는 함수가 다음과 같은 두 가지 상태를 만족해야 한다는 개념
확장에는 열려 있어야 한다 (Open for extension):
새로운 기능을 추가하거나 요구사항의 변화에 따라 시스템의 동작을 확장할 수 있어야 한다.
기존 코드를 수정하지 않고도 기능 추가가 가능해야 한다.
수정에는 닫혀 있어야 한다 (Closed for modification):
기존의 잘 검증된 코드는 수정되지 않아야 한다.
코드 변경이 없기 때문에 기존 시스템의 안정성을 유지할 수 있다.
목표
코드를 재사용 가능하고 유지보수성이 높은 상태로 유지.
새로운 요구사항이 생기더라도 기존 코드의 변경 없이 대응 가능.
classNotification{
publicvoidsend(String type){
if (type.equals("Email")) {
// 이메일 전송 코드
} elseif (type.equals("SMS")) {
// SMS 전송 코드
}
}
}
--------------------------------------------
interfaceNotification{
voidsend();
}
classEmailNotificationimplementsNotification{
publicvoidsend(){
// 이메일 전송 코드
}
}
classSMSNotificationimplementsNotification{
publicvoidsend(){
// SMS 전송 코드
}
}
classPushNotificationimplementsNotification{
publicvoidsend(){
// 푸시 알림 전송 코드
}
}
classNotificationService{
publicvoidsendNotification(Notification notification){
notification.send();
}
}
새로운 알림 방식(예: 푸시 알림)을 추가하려면 send 메서드에 조건문을 추가해야 하므로 기존 코드를 수정해야 함.
=> 새로운 알림 방식을 추가하려면 새로운 클래스를 구현하기만 하면 된다. 기존 코드는 수정하지 않는다.
OOP (Object-Oriented Programming)
📌 객체 지향 프로그래밍(OOP)은 현실 세계의 사물이나 개념을 객체(Object)로 모델링하여 소프트웨어를 개발하는 방식
객체를 사용해 데이터를 묶고, 이를 다루는 기능을 추가합니다.
[1] 캡슐화 (Encapsulation)
내부 데이터를 보호하고, 외부에서 접근할 방법을 제한하는 것.
약병은 뚜껑으로 약을 보호하지만, 우리가 약을 꺼낼 수 있는 기능(뚜껑 열기)을 제공해요.
[2] 상속 (Inheritance)
부모가 가진 것을 자식이 물려받는 것.
부모가 물려준 성격(코드)을 자식이 사용하거나 더 발전시킬 수 있어요!
[3] 다형성 (Polymorphism)
같은 동작을 다양한 방식으로 처리할 수 있는 것.
“달려!“라는 명령에 강아지는 뛰고, 자동차는 굴러가요. 하지만 둘 다 “달리는 동작”이에요!
[4] 추상화 (Abstraction)
복잡한 내부 내용은 숨기고, 필요한 부분만 드러내는 것.
자동차의 내부 작동 방식은 몰라도, 운전대와 페달로 운전할 수 있잖아요!
SOLID 원칙 - 면접에서 자주 나온다
📌 OOP를 더 잘 설계하기 위한 5가지 규칙.
쉽게 고치고, 확장할 수 있는 코드를 만드는 지침.
[1] S - 단일 책임 원칙
클래스는 하나의 역할만 가져야 한다.
주방은 요리만, 침실은 잠만. (하나의 역할만!)
왜?
역할이 여러 개면 고칠 때 어디를 수정해야 할지 복잡해 짐.
publicclassUser{
private String name; // 사용자 정보publicvoidlogin(){ /* 로그인 기능 */ }
publicvoidsaveUser(){ /* 데이터베이스 저장 기능 */ }
}
--------------------------
publicclassUser{ /* 사용자 정보 관리 */ }
publicclassAuthService{
publicvoidlogin(User user){ /* 로그인 기능 */ }
}
publicclassUserRepository{
publicvoidsaveUser(User user){ /* 데이터베이스 저장 */ }
}
[2] O - 개방/폐쇄 원칙
코드는 확장에 열려 있고, 수정에는 닫혀 있어야 한다.
옥상에 방을 추가해도 기존 방은 그대로. (확장 가능!)
왜?
기존 코드를 수정하면 예기치 못한 문제가 생길 수 있음.
원래라면 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야 한다. 하지만 개방 폐쇄 원칙을 적용하면 새로운 도형이 추가되더라도 shape 인터페이스만 구현하면 된다.
다형성을 활용하여 해결한다.
인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
publicclassShape{
public String type;
}
publicclassAreaCalculator{
publicdoublecalculate(Shape shape){
if (shape.type.equals("circle")) {
return/* 원의 넓이 계산 */;
} elseif (shape.type.equals("square")) {
return/* 사각형의 넓이 계산 */;
}
}
}
------------------------
publicinterfaceShape{
doublecalculateArea();
}
publicclassCircleimplementsShape{
publicdoublecalculateArea(){ return/* 원의 넓이 계산 */; }
}
publicclassSquareimplementsShape{
publicdoublecalculateArea(){ return/* 사각형의 넓이 계산 */; }
}
publicclassAreaCalculator{
publicdoublecalculate(Shape shape){
return shape.calculateArea();
}
}
문제점
// Circle을 계산하는 경우publicclassMain{
publicstaticvoidmain(String[]){
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle();
areaCalculator.calculate(circle);
}
}
// Square를 계산하는 경우publicclassMain{
publicstaticvoidmain(String[]){
AreaCalculator areaCalculator = new AreaCalculator();
// Circle circle = new Circle();
Square square = new Square();
areaCalculator.calculate(square);
}
}
구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 한다.
객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다.
Spring Container의 역할
[3] L - 리스코프 치환 원칙
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
새가 날 수 있는 것처럼, 모든 새는 ‘날기’ 동작을 가져야 한다. (일관성 유지!)
부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
왜?
부모 클래스처럼 동작하지 않으면, 프로그램이 예외를 일으킴.
예시
ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없다. LSP 위반
리스코프 치환 원칙 적용
인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.
classCar{
publicvoidaccelerate(){
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
classElectricCarextendsCar{
@Overridepublicvoidaccelerate(){
thrownew UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
publicclassMain{
publicstaticvoidmain(String[] args){
Car car = new Car();
car.accelerate(); // "자동차가 가속합니다."
Car electricCar = new ElectricCar();
electricCar.accelerate(); // UnsupportedOperationException 발생
}
}
---------------------------------
// 가속 기능(역할)을 인터페이스로 분리interfaceAcceleratable{
voidaccelerate();
}
classCarimplementsAcceleratable{
@Overridepublicvoidaccelerate(){
System.out.println("내연기관 자동차가 가속합니다.");
}
}
classElectricCarimplementsAcceleratable{
@Overridepublicvoidaccelerate(){
System.out.println("전기차가 배터리로 가속합니다.");
}
}
publicclassMain{
publicstaticvoidmain(String[] args){
Acceleratable car = new Car();
car.accelerate(); // "내연기관 자동차가 가속합니다."
Acceleratable electricCar = new ElectricCar();
electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
}
}