객체지향 코드 적용하기
객체 지향 이론을 코드로 적용하기
상속과 조합
상속 구조는 과거에 코드 반복이 하드웨어 성능에 영향을 끼친다고 생각 했던 시절에 부모 클래스의 메서드를 자식 클래스에서 반복 없이 사용 할 수 있을 때 효율적이었다.
하지만 현대 하드웨어 성능이 과연 코드 몇 줄 줄였을 때 유의미한 성능 향상을 기대할 수 있을까? 단연 있다고 한들 굉장히 견고한 설계 구조가 아닌 이상 쉽게 상속 구조를 사용하기 어렵다.
유지보수 하는 개발자 입장에서 부모 클래스의 메서드 하나를 수정하기 위해 굉장히 많은 작업을 반복해야 하기 때문에 유연한 조합 설계 구조로 왠만한 문제는 풀어나갈 수 있다.
Example use case:
public class EmptyCell extends Cell {
private static final String EMPTY_SIGN = "■";
@Override
public boolean isLandMine() {
return false;
}
@Override
public boolean hasLandMineCount() {
return false;
}
@Override
public String getSign() {
if (isOpened) { // 부모 메서드
return EMPTY_SIGN;
}
if (isFlagged) { // 부모 메서드
return FLAG_SIGN;
}
return UNCHECKED_SIGN;
}
}
Value Object(VO)
Example use case:
public class Money {
private final long amount;
public Money(long amount){
if (amount < 0) {
throw new IllegalArgumentException("금액은 0원 이상이어야 합니다.);
}
this.amount = amount;
}
// equals() && hashCode() 재정의
}
/*
Money money1 = new Money(1_000L);
Money money2 = new Money(1_000:);
assertThat(money1 == money2).isFalse();
assertThat(money1.equals(money2)).isTrue();
*/
VO vs Entity
class UserAccount {
private String userId; // 식별자
private String 이름;
private String 생년월일;
private Address 집주소;
}
Entity는 식별자가 존재한다. 식별자가 아닌 필드의 값이 달라도 식별자가 같으면 동등한 객체로 취급한다.
equals()
&hashCode()
도 식별자 필드만 가지고 재정의 할 수 있다.식별자가 같은데 식별자가 아닌 필드의 값이 서로 다른 두 인스턴스가 있다면 같은 Entity가 시간이 지남에 따라 값이 수정된 것으로 이해 할 수 있다.
VO는 식별자 없이 내부의 모든 값이 다 같아야 동등한 객체로 취급한다
개념적으로 전체 필드가 다 같이 식별자 역할을 한다고 생각해도 된다.
일급 컬렉션
컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체
컬렉션을 다른 객체와 동등한 레벨로 다루기 위함
단 하나의 컬렉션 필드만을 가진다
컬렉션을 추상화하며 의미를 담을 수 있고 흩어져 있던 가공 로직을 객체 내부로 옮길 수 있다.
가공 로직에 대한 테스트도 가능
만약
getter
로 컬렉션을 반환 할 일이 생긴다면 포장 된 컬렉션을 반환 하지 말 것!Private attribute를 외부에 그대로 제공 했을 때 컬렉션이기 때문에 조작이 가능 함
조작을 하게 되면 동일한 메모리 주소를 갖고 있어 내부 로직에 영향을 받을 수 있음
그렇기 때문에 외부 조작을 피하기 위해 꼭 새로운 컬렉션으로 만들어서 반환 해주자
일급 시민
다른 요소에게 사용 가능한 모든 연산을 지원하는 요소
변수로 할당 될 수 있음
파라미터로 전달 될 수 있음
함수의 결과로 반환 될 수 있음
Example use case:
public class Money {
private final List<CreditCard> cards;
// 생성자
public List<CreditCard> findValidCards() {
return this.cards.stream()
.filter(CreditCard::isValid)
.toList();
}
}
Enum의 특성과 활용
Enum은 상수의 집합이며, 상수와 관련된 로직을 담을 수 있는 공간이다.
상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체
특정 도메인 개념에 대해 그 종류와 기능을 명시적으로 표현 해줄 수 있다.
만약, 변경이 정말 잦은 개념은 Enum 클래스 보다 DB로 관리하는 것이 나을 수 있다.
Tips
Enum을 만들 때 제3자가 읽을 때 해석 하는 데 도움을 줄 설명을 한글로 작성 한다.
Example use case:
public enum CellSnapshotStatus {
EMPTY("비어있는 셀"),
FLAG("깃발"),
LAND_MINE("지뢰"),
NUMBER("숫자"),
UNCHECKED("확인 전");
private final String description;
CellSnapshotStatus(String description) {
this.description = description;
}
}
다형성 활용하기
Enum의 특성 활용하기에서 확인 했던 Enum의 사용하기 예시에 문제점이 있다. 상태에 따른 조건문이 굉장히 많다는 것이고, 상태가 늘어날 때 마다 조건이 추가 되는 매우 큰 코드 덩어리를 만들고 있다는 것이다.
변하는 것과 변하지 않는 것을 분리하여 추상화 하고, OCP를 지키는 구조
"반복적인 if문을 단순하게 만들 수 없을까?"
어떤 조건을 만족 하면, 어떠한 행위를 수행하는 것은 반복적인 if문을 만들어낸다.
변화 하는 것: 구체
변하지 않는 것: 추상

다형성을 위한 인터페이스 구현:
public interface CellSignProvidable {
boolean supports(CellSnapshot cellSnapshot);
String provide(CellSnapshot cellSnapshot);
}
Example use case:
public class CellSignFinder {
private static final List<CellSignProvidable> CELL_SIGN_PROVIDERS = List.of(
new EmptyCellSignProvider(),
new FlagSignProvider(),
new LandMineCellSignProvider(),
new NumberCellSignProvider(),
new UncheckedCellSignProvider()
);
public String findCellSignFrom(CellSnapshot snapshot) {
return CELL_SIGN_PROVIDERS.stream()
.filter(provider -> provider.supports(snapshot))
.findFirst()
.map(provider -> provider.provide(snapshot))
.orElseThrow(() -> new IllegalArgumentException("확인 할 수 없는 셀 입니다."));
}
숨겨져 있는 도메인 개념 도출하기
Example use case:
public class GameApplication {
public static void main(String[] args) {
GameLevel gameLevel = new VeryBeginner();
ConsoleInputHandler consoleInputHandler = new ConsoleInputHandler();
ConsoleOutputHandler consoleOutputHandler = new ConsoleOutputHandler();
Minesweeper minesweeper = new Minesweeper(gameLevel, consoleInputHandler, consoleOutputHandler);
minesweeper.initialize();
minesweeper.run();
}
요약
[ 상속과 조합 ]
상속 설계를 선택 하면 부모와 자식 관계의 결합도가 굉장히 높아져, 부모의 코드 변경이 모든 자식에게 영향을 미치게 되는 단점이 존재했다.
그에 반면, 중복 되는 코드를 제거 할 수 있다는 장점이 있지만 이 단점을 감안하지 않더라도 같은 기대치를 안고 있는 조합으로 재설계 할 수 있었다.
[ Value Object, Entity ]
의미 없는 원시 타입의 객체는 어플리케이션 내에서 객체간 협력을 할 때 아무런 역할을 하지 못하는 관계이다.
이런 객체에 의미를 부여하여 협력 관계에서 역할을 제공 할 수 있다.
VO vs Entity는 유명한 주제인데 둘의 차이는 식별자가 있냐, 없냐의 차이를 나타내며 식별자가 있다면 "Entity", 없다면 "VO"로 이해할 수 있다.
VO 객체를 설계 할 때 필요한 3가지 특성
불변성
동등성
유효성 검증
[ 일급 컬렉션 ]
VO와 비슷한 개념이지만 일급 컬렉션은 원시 타입이 아닌 컬렉션을 래핑하여 의미를 부여한 객체이다.
컬렉션을 포장 하면서 해당 컬렉셔 내 데이터를 가공 하거나, 도메인 영역에서 다룰 수 있는 문제를 저장 하는 공간을 만들어낼 수 있는 추상화 도구이다.
일급 컬렉션에 대한 getter
를 제공 할 때는 객체 내 데이터를 그대로 반환 하지말고 꼭 새로운 객체로 반환 해야한다. 즉, 메모리 주소를 공유 하여 외부에서 컬렉션에 대한 수정을 일으켰을 때 객체 내 데이터가 흔들리지 말아야한다.
[ Enum ]
상수의 집합인 열거형 타입이며 상수를 처리하는 로직도 담을 수 있는 객체이다.
[ 추상화와 다형성을 활용하여 반복되는 if문 제거, OCP 지키기 ]
위 Enum을 잘 활용 하면 다형성을 손 쉽게 적용 할 수 있었는데, 그 중 인터페이스를 Enum에서 구현 하여 조건에 따라 바뀌는 행동에 대한 조건문을 제거 할 수 있었고 OCP 원칙을 지키기 위해 변하는 것과 변하지 않는 내용을 구분해서 보는 시각을 기를 수 있다.
[ 숨겨져 있는 도메인 개념 도출하기 ]
객체지향은 100% 현실을 반영 한 것이 아닌 모방이기 때문에 우리 도메인에서 드러나지 않는 개념을 도출해내는 노력이 필요한데, 그러기 위해선 어플리케이션의 미래를 고려하여 변경이 생길 것 같은 지점에 숨겨진 도메인 개념을 찾아보고 변경하는 과정이다.
Last updated