본문 바로가기
BE 📙/디자인패턴의 아름다움

[CH03.01] 설계 원칙 - 단일 책임 원칙

by 경아ㅏ 2025. 8. 5.

단일 책임 원칙의 정의 및 해석

 

클래스와 모듈은 하나의 책임 또는 기능을 가지고 있어야 한다는 설계 원칙

* 여기서 모듈을

 - 추상적인 개념으로 클래스도 일종의 모듈이라고 정의하거나

 - 클래스보다 포괄적인 개념으로 여러개의 클래스가 모여 모듈이 된다고 정의 가능

그렇지만 모듈을 어느 개념으로 정의하더라도 단일 책임 원칙의 개념을 적용하는데 큰 지장은 없다

클래스에 비즈니스와 관련 없는 기능이 두 개이상 포함되어있으면 단일 기능을 가진 더 작은 클레스로 분할 해야 한다.



클래스에 단일 책임이 있는지 판단하는 방법

 

[주문 관련 기능 vs 사용자 관련 기능] 과 같은 예시는 서로 다른 기능임을 구분하기 쉬우나, 다음과 같은 상황에서 클래스가 단일 책임 원칙을 만족하는지 판단하는 것은 쉽지 않다.

public class UserInfo {
    private long userId;
    private String username;
    private String email;
    private String telephone;
    private long createTime;
    private long lastLoginTime;
    private String avatarUrl;
    private String provinceOfAddress;  // 도
    private String cityOfAddress;      // 시
    private String regionOfAddress;    // 구
    private String detailedAddress;    // 상세 주소
    // ...
}



1) UserInfo 는 사용자와 관련된 정보만을 포함하고 있어 단일 책임 원칙을 충족한다는 관점
2) UserInfo 클래스에서 주소 정보의 비율이 상대적으로 높기 때문에 주소 관련된 정보들을 UserAddress 클래스로 분할해야 한다는 관점

 


해당 관점을 생각해보는 시나리오(상황)에 따라 1번이 나은 선택일 수도 있고, 2번이 나은 선택일 수도 있다.

 

  • UserInfo 내에 있는 모든 변수들이 사용자 정보를 표시하는데만 사용된다면, UserInfo 내에 단일 기능으로 남아있을 수 있다.
  • 반면, 전자 상거래 기능이 추가되어 사용자의 주소 정보가 물류 정보를 처리하는데 독립적으로 사용된다면 UserAddress 클래스로 분리하는 것이 좋다.

 

비즈니스 수준의 관점에 따라 단일 책임을 판별할 수 있다.

 

  • 사용자 비즈니스 수준의 관점에서는 모두 사용자 정보이므로 단일 책임 원칙을 충족한다.
  • 사용자 표시정보, 주소 정보, 로그인 인증 정보처럼 세분화된 비즈니스 수준에서 각각 살펴보면 단일 책임을 충족하지 않으므로 더 작은 단위로 분할해야 한다.

 

## 단일 책임 여부를 결정하기 위한 몇가지 원칙

1) 클래스에 속성, 메서드가 너무 많아 가독성과 유지보수성이 떨어지면 분할 고려하기

2) 클래스가 다른 클래스에 과하게 의존한다면 클래스 분할 고려하기

3) 클래스에 private 메서드가 너무 많은 경우 다른 클래스로 분리하고 public 으로 전환하여 코드 재사용성 높이기
4) 클래스 이름을 정확하게 지정하기 어렵다면, 혹시 클래스 책임 정의가 불분명한 것은 아닌지 생각해보기

 


꼭 클래스를 작게 분할하는 것이 좋을까?


직렬화와 역직력화 클래스를 작성한다고 할 때, 

 

/**
 * Protocol format: identifier-string;{gson string}
 * For example: UEUEUE;{"a":"A","b":"B"}
 */
public class Serialization {
    private static final String IDENTIFIER_STRING = "UEUEUE;";
    private Gson gson;

    public Serialization() {
        this.gson = new Gson();
    }

    public String serialize(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder();
        textBuilder.append(IDENTIFIER_STRING);
        textBuilder.append(gson.toJson(object));
        return textBuilder.toString();
    }

    public Map<String, String> deserialize(String text) {
        if (!text.startsWith(IDENTIFIER_STRING)) {
            return Collections.emptyMap();
        }
        String gsonStr = text.substring(IDENTIFIER_STRING.length());
        return gson.fromJson(gsonStr, Map.class);
    }
}

 

 

위의 클래스를 단일 책임 원칙 기반으로 직렬화 / 역직력화 기능으로  나누면,

 

// 직렬화 기능만 담당하는 클래스
public class Serializer {
    private static final String IDENTIFIER_STRING = "UEUEUE;";
    private Gson gson;

    public Serializer() {
        this.gson = new Gson();
    }

    public String serialize(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder();
        textBuilder.append(IDENTIFIER_STRING);
        textBuilder.append(gson.toJson(object));
        return textBuilder.toString();
    }
}

// 역직렬화 기능만 담당하는 클래스
public class Deserializer {
    private static final String IDENTIFIER_STRING = "UEUEUE;";
    private Gson gson;

    public Deserializer() {
        this.gson = new Gson();
    }

    public Map<String, String> deserialize(String text) {
        if (!text.startsWith(IDENTIFIER_STRING)) {
            return Collections.emptyMap();
        }
        String gsonStr = text.substring(IDENTIFIER_STRING.length());
        return gson.fromJson(gsonStr, Map.class);
    }
}

 

 

Serializer 클래스와 Deserializer 클래스는 단일 책임 원칙은 충족하게 되지만 해당 코드를 변경할 때 문제가 발생한다. 데이터 식별자를 "UEUEUE;" 에서 다른 것으로 변경하거나 직렬화 메서드를 JSON에서 XML로 변경한다면 양 쪽 모두 수정되어야 하고, 한 쪽을 수정하지 않는 경우 잘못된 실행 결과를 초래할 수 있다. 결국, 설계 원칙은 반드시 적용해야 하는 것이 아니고, 가독성과 유지보수성을 기준으로 어느 방식이 타당한지 판단하며 적용해야 한다.

 

 

'BE 📙 > 디자인패턴의 아름다움' 카테고리의 다른 글

[CH 05.01, 05.02] 리팩토링, 단위테스트  (4) 2025.08.12
[CH02.09] 상속보다 합성  (1) 2025.08.04
[CH03.07-03.08] 설계원칙  (4) 2025.07.31
[CH01] 개요  (5) 2025.07.21

댓글