Home 상속과 컴포지션
Post
Cancel

상속과 컴포지션

상속 (Inheritance)

상속은 객체 지향 프로그래밍에서 기존 클래스의 특성과 기능을 그대로 물려받아 새로운 클래스를 정의하는 것이다.

이는 클래스 간의 “is-a” 관계를 표현하며, 코드를 재사용하고 클래스 간의 관계를 명확히 할 수 있다.

ex. 동물 클래스가 있고, 이를 상속받은 고양이, 개 클래스가 있을 때, 고양이와 개는 모두 동물이라는 공통 특성을 갖는다.



장점

코드 재사용성

부모 클래스의 기능을 자식 클래스가 그대로 이용할 수 있다.



다형성 구현

다형성이란 한 가지 인터페이스나 기능을 여러 방식으로 구현할 수 있는 것을 의미한다.

부모 클래스의 메서드를 자식 클래스에서 오버라이딩하여 다른 동작을 구현할 수 있다.


*다형성의 대표적인 예시로는 오버로딩(Overloading)과 오버라이딩(Overriding)이 있다.

Overloading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 같은 이름의 메서드를 여러 개 정의하되 매개변수의 타입, 개수, 순서가 다른 경우
public class Example {
    public void print(int num) {
        System.out.println("정수: " + num);
    }

    public void print(double num) {
        System.out.println("실수: " + num);
    }

    public void print(String str) {
        System.out.println("문자열: " + str);
    }
}


Overriding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 상속 관계에서 부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용
class Animal {
    public void makeSound() {
        System.out.println("Animal");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat");
    }
}




단점

캡슐화 위반

자식 클래스에서 부모 클래스의 메서드를 오버라이딩하면, 부모 클래스의 의도와 다른 동작이 수행될 수 있다.


캡슐화, 즉 정보 은닉은 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것을 의미한다.

이를 통해 우리는 클래스의 기능을 사용할 때 내부 동작을 알 필요없이 단순히 메서드만 호출할 수 있다.

단, 내부 동작을 알 필요가 없다는 말은 신뢰성이 보장되어야 한다는 말이기도 하다.

따라서 캡슐화가 위반된다면 이는 신뢰성이 깨진 것으로 볼 수 있다.


아래의 예시 코드에서 Dog 클래스가 Animal 클래스의 makeSound 메서드를 오버라이딩하여 동작을 변경한다.

이렇게 부모 클래스의 동작을 변경함으로써 캡슐화가 위반된다고 볼 수 있다.

1
2
3
4
5
6
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

결국, 상속 후에 진행된 오버라이딩은 캡슐화를 위반할 수 있다는 것을 염두에 두어야 한다.



결합도 증가

결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 의존 정도를 말한다.

객체지향 프로그래밍에서는 결합도는 낮을수록, 응집도는 높을수록 좋다.

그래서 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화 할 수 있다.


여기서 상속을 하게 되면 부모 클래스의 내부 구현에 의존하기 때문에

부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 한다.

아래와 같이 코드에서 Food 부모 클래스에 count 필드를 하나 추가해버리면,

자식클래스는 물론 클래스 호출 부분 까지 전부 수정해주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Food {
    final int price;
    final int count; // 코드 추가
    
    Food(int price, int count) { 
        this.price = price;
        this.count = count; // 코드 추가
    }
}

class Bread extends Food {
    public Bread(int price, int count) {
        super(price, count); // 코드 수정
    }
}

public class Main {
    public static void main(String[] args) {
        Food bread = new Bread(1000, 5); // 코드 수정
    }
}



불필요한 기능 상속

부모 클래스에 추가된 메서드가 자식 클래스에 적합하지 않는 경우도 있다.

Animal 클래스에 fly() 라는 메서드를 추가했을때, Tiger 자식 클래스에서는 동작하지 않는 메서드가 되어 버린다.




상속을 사용하는 경우는 명확한 is - a 관계에 있는 경우,

상위 클래스가 확장할 목적으로 설게되었고 문서화도 잘되어 있는 경우에 사용하면 좋다.






컴포지션 (Composition)

컴포지션은 한 클래스가 다른 클래스의 인스턴스를 포함하는 것이다.

포함된 객체의 기능을 사용하여 클래스의 기능을 확장하거나 구현한다. “has-a” 관계를 나타낸다.

ex. 자동차 클래스가 엔진 클래스의 인스턴스를 포함하여 자동차가 엔진을 가지고 있다.



낮은 결합도

컴포지션을 사용하면 클래스 간의 결합도가 낮아진다.

각 클래스는 독립적으로 존재할 수 있으며, 변경 사항이 다른 클래스에 영향을 미치지 않는다.



유연성

컴포지션을 사용하면 클래스를 구성하는 객체를 동적으로 변경할 수 있다.

즉, 클래스의 기능을 유연하게 확장하거나 수정할 수 있다.






REFACTORING

내가 수정할 부분은 LastMessage와 ChatRoomDto다.

이 두 개의 dto에는 중복된 변수들이 많고 LastMessage에는 nickname 필드만 존재하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
public class ChatRoomDto implements Serializable { 
    private String roomId; 
    private String nickname;
    private Integer adminChat;
    private Integer userChat;
    private String message;
    private String day;
    private String time;

    public ChatRoomDto() {
    }

    public static ChatRoomDto create() {
        ChatRoomDto room = new ChatRoomDto();
        room.roomId = UUID.randomUUID().toString();
        return room;
    }

    public static ChatRoomDto of(String roomId, User user, LastMessage lastMessage) {
        return ChatRoomDto.builder()
                .roomId(roomId)
                .nickname(user.getNickname())
                .adminChat(lastMessage.getAdminChat())
                .userChat(lastMessage.getUserChat())
                .message(lastMessage.getMessage())
                .day(lastMessage.getDay())
                .time(lastMessage.getTime())
                .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Getter
@Builder
public class LastMessage {
    private String roomId;
    private int adminChat;
    private int userChat;
    private String message;
    private String day;
    private String time;

    public static LastMessage of(ChatMessage chatMessage, int adminChat, int userChat, String day, String time){
        return LastMessage.builder()
                .roomId(chatMessage.getRoomId())
                .adminChat(adminChat)
                .userChat(userChat)
                .message(chatMessage.getMessage())
                .day(day)
                .time(time)
                .build();
    }
}




중복 제거와 Composition 활용

내가 수정하는 DTO 는 명확한 is-a 관계는 아니지만, 완전히 관계가 없는 것은 아니다.

또한 메서드 안에서 두 객체가 함께 사용되기 때문에 중복을 최대한 제거하고자 했다.


중복 제거와 유연성을 위해 Composition을 활용하여 refactoring을 진행했다.


따라서 ChatRoomDto가 LastMessage를 포함함으로써, ChatRoomDto에서는 LastMessage의 일부 기능을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Getter
@Setter
@Builder
@AllArgsConstructor
public class ChatRoomDto implements Serializable { 
   private String roomId;
   private String nickname;
   private LastMessage lastMessage;// Composition(has-a 관계)

   public ChatRoomDto() {
   }

   public static ChatRoomDto create() {
      ChatRoomDto room = new ChatRoomDto();
      room.roomId = UUID.randomUUID().toString();
      return room;
   }

   public static ChatRoomDto of(User user, LastMessage lastMessage) {
      return ChatRoomDto.builder()
            .nickname(user.getNickname())
            .lastMessage(lastMessage)
            .build();
   }
}

Service에서는 ChatRoomDto.of(roomId, user, lastLine);ChatRoomDto.of(user, lastLine);와 같이 수정했다.




컴포지션을 사용하면 클래스 간의 결합도가 낮아져 클래스를 구성하는 객체를 동적으로 변경할 수 있다.

이는 코드의 수정 없이도 ChatRoomDto의 기능을 유연하게 확장할 수 있다는 것을 의미한다.

예를 들어 ChatRoomDto의 기능을 확장하기 위해 LastMessage에 새로운 변수나 메서드를 추가해야 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
@Builder
public class LastMessage {
    private String roomId;
    private int adminChat;
    private int userChat;
    private String message;
    private String day;
    private String time;
    
    // 새로운 변수나 메서드 추가
    private boolean hasAttachment;
    private String attachmentUrl;
    
}

이렇게 LastMessage에 새로운 기능을 추가하면, ChatRoomDto에서는 별도의 수정 없이도 이를 활용할 수 있다.

이는 클래스 간의 결합도가 낮아서 발생하는 유연성의 장점이다.

따라서 코드의 수정 없이도 ChatRoomDto의 기능을 유연하게 확장할 수 있다는 것을 의미한다.



중복된 변수를 최소화하여 코드를 개선하고, 컴포지션을 통해 클래스 간의 결합도를 낮췄다.

이를 통해 코드의 유지보수성을 높일 수 있고, 더욱 유연하고 확장 가능한 구조를 만들 수 있다.





REFERENCE

This post is licensed under CC BY 4.0 by the author.