공부내용공유

java 태그 달린 클래스 vs 계층 구조 (feat : 오브젝트 chap 7 객체 분해) 본문

Server/Java

java 태그 달린 클래스 vs 계층 구조 (feat : 오브젝트 chap 7 객체 분해)

forfun 2024. 7. 7. 23:16

서론


 

이펙티브 자바 스터디에서 item 23 태그달린 클래스보다는 클래스 계층 구조를 활용해라라는 주제에 대해 팀원들과 얘기를 나누고 얼마 지나지 않아 오브젝트라는 도서에서 비슷한 주제를 보게 되었다.

 

이번 글에서는 두 책에서 다루는 클래스 계층 구조 (상속) 과 태그 달린 클래스 방식의 장,단점을 정리할 예정이다.

 

 

본론


이 글의 목차는

  • 태그 달린 클래스
  • 계층 구조를 활용한 클래스
  • 어떤게 더 좋을까

로 이루어져있다.

 

 

태그 달린 클래스

 

태그 달린 클래스란 무엇일까? 코드로 예시를 보면 빠르게 이해가 가능할 것이다.

public class User {
    long id;
    String name;
    long level;
    Address address;
    UserType userType;
}

public enum UserType {
    ADMIN, NORMAL, VIP, ANONYMOUS
}

 

이렇게 클래스의 타입을 특정한 변수로 나타낸 클래스를 의미한다. 해당 타입에 관련하여 특별한 요구사항 없다면 이렇게 필드를 통해 구분을 해도 크게 문제가 없을 것이다, 하지만 해당 타입에 관련하여 여러 요구사항이 있고 로직을 작성해야 한다면 얘기가 달라진다.

 

 

사용자가 특정 상품을 구매할 때 사용자 타입에 따라 할인률이 달라진다와 같은 요구사항이 있다고 해보자.

해당 요구사항에 대한 로직을 작성하게 된다면

public class User {
    long id;
    String name;
    long level;
    Address address;
    UserType userType;

    public int getDiscountRate() {
        switch (userType) {
            case ADMIN -> return 10;
            case NORMAL -> return 3;
            ...
        }
    }
}

 

 

이런식으로 user 안에 구현되거나 가격을 계산하는 client 코드에서 타입별로 할인률을 받아서 가격을 계산하게 로직을 작성할 수 있을 것이다.

UserType userType = user.getUserType();
int discountRate = 0;
switch (userType) {
    case ADMIN -> discountRate = 10;
    case NORMAL -> discountRate = 3;
    ... 
}

 

이펙티브 자바에서도 지적하고있는 이러한 코드의 문제점은 무엇일까? 흔히 볼 수 있는 문제로

  1. OCP 위배 - 유저 타입이 추가되게 되면 해당 분기문을 사용하는 코드들이 변경되야 한다.
  2. SRP 위배 - 현재 요구사항은 문제가 없지만 유저 타입별로 다른 로직을 수행하는 코드가 생기게 되면 필요없는 다른 타입의 로직이 클래스에 모두 남게된다.

이와 같은 것들이 있을 것이다.

 

 

계층 구조를 활용한 클래스

 

그렇다면 계층 구조를 활용한 클래스는 무엇일까? 이것은 말 그대로 상속을 이용한 방식이다.

public abstract class User {
    long id;
    String name;
    long level;
    Address address;

    public abstract int getDiscountRate() {

    } 
}

public class AdminUser extends User{

    @Override
    public int getDiscountRate() {
        return 10;
    }
}

public class VIPUser extends User {
    @Override
    public int getDiscountRate() {
        return 15;
    }
}

 

이런식으로 활용이 가능하다. 이렇게 함으로 유저 타입별로 각자의 로직만 가지면 되고 유저 타입이 추가되어도 기존 코드 변경을 하지 않아도 되다는 장점이 있다.

 

 

어떤게 더 좋을까

 

이펙티브 자바 item 23에서는 제목에서도 알 수 있듯이 태그 달린 클래스보다는 계층 구조를 활용하하고 나와있다, 물론 함부로 상속을 활용하면 안된다는 주의도 하고 있고 이러한 내용은 item 18에서 자세히 다루고 있다.

 

 

그리고 오브젝트에서는 추상 데이터 타입절차 추상화 라는 용어로 이와 비슷한 내용을 설명한다.

 

추상 데이터 타입

추상 데이터 타입은 말 그대로 데이터 타입을 추상화 한 것을 의미한다.

 

출처 : 오브젝트

 

오브젝트에서 설명하는 Employee는 Employee라는 타입 1개로 표현되고 있지만 내부에서는 여러개의 타입을 포괄하고 있다.

 

절차 추상화

현재 널리 퍼진 객체 지향이라는 개념이 절차 추상화라고 볼 수 있다.

 

출처 : 오브젝트

 

 

추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는다면 객체 지향은 오퍼레이션을 기반으로 타입을 묶는다.

 

이러한 추상화를 통해 동일한 메세지에 대해 다르게 반응할 수 있고(다형성) 클라이언트는 각 타입을 구분할 필요가 없게되므로
객체지향적인 코드를 작성할 수 있다.

 

그래서 뭐가 더 나을까?

 

그렇다면 무조건 상속을 이용한 절차 추상화 방식이 맞는걸까? 그렇지 않다. 해당 구현 방식을 보면 쉽게 예상할 수 있고 오브젝트에서도 설명을 해주는데

  1. 타입이 변경될 확률이 높다.
    1. 객체 지향이 더 유리하다. (타입이 추가되어도 클라이언트 코드는 변경될 필요 없다.)
    2. 타입이 추가되어도 기존 타입 코드는 변경될 필요가 없다.
  2. 타입 보다는 타입에 대한 오퍼레이션이 추가될 확률이 높을 경우
    1. 추상 데이터 타입이 더 유리하다.
    2. 만약 객체지향 방식에서 오퍼레이션이 추가된다면 모든 상속된 코드에 오퍼레이션을 추가해야 한다.
    3. 추상 데이터 타입은 전체 타입에 대한 코드가 구현체 내에 들어가있기 때문에 변경이 상대적으로 간단하다.

 

상속은 유연한 코드를 만들어주지만 코드의 구조를 복잡하게 만들고 부모 클래스의 캡슐화를 깨기도한다, 때문에 그 트레이드 오프를 잘 생각하여 구현 방식을 결정해야 한다.

 

오브젝트에서 언급한 위 내용도 좋은 기준이고 개인적으로 내가 만났던 타입을 구별해야하는데 별다른 오퍼레이션이 없는 경우에도 굳이 상속을 사용하여 복잡도를 높이는 것 보단 조금은 덜 깔끔한? (switch 등을 사용하는) 코드를 작성하더라도 타입을 통해 구분하는 방식을 사용하는게 로직을 파악하고 사용하는데 용이할거라고 생각한다.

 

 

 

결론


Type을 사용한 구분은 흔히들 안티패턴이라하고 나도 단순히 그렇게만 생각을 했었는데 오브젝트를 공부하면서 이러한 방식의 네이밍과 역사(?)를 알게되고 장,단점을 좀 더 명확히 정리하게 되어 좋았다.