HJW's IT Blog

단일 책임 원칙, 얼마나 쪼개야 ‘적당함’일까? 본문

개발 개념

단일 책임 원칙, 얼마나 쪼개야 ‘적당함’일까?

kiki1875 2025. 1. 3. 17:21

들어가며

 

SRP, 어디까지 적용되어야 할까.. 는 코드를 작성할 때 항상 고민하던 부분 같다. 과도하게 분리하게 되면 오히려 복잡도와 가독성이 떨어지는 것 같고, 분리를 하지 않는다면 하나의 메소드 혹은 클래스가 너무 많은 책임을 지는 것 같았다.

예를 들어 UserManager 라는 클래스가 있다면. 이 클래스의 책임 범위는 어디까지 일까?

  • 사용자에 관련된 모든 기능?
  • 간단한 CRUD?
  • Login, Logout 까지만?

명확한 답을 내리기 힘든 질문 인 것 같다.

 

 


 

 

첫생각

 

public class UserManager {
    public void createUser() { ... }
    public void updateUser() { ... }
    public void deleteUser() { ... }

    public void login() { ... }
    public void logout() { ... }

    public void resetPassword() { ... }

    // 사용자 관련 설정, 권한 관리, 기타 메서드들...
}

 

위 클래스를 보면 여러 책임이 혼합되어 있다. 사용자 정보 관리, 비밀번호 관리, 등…

우선 다른 책임으로 분리될 수는 없을까에 대해 생각해 보았다. 단순히 사용자 관리는 너무 광범위 하다.

  • Authentication
  • Authorization
  • 비밀번호 재설정,
  • 혹은 기타 비즈니스 로직이 속할 것이다

이러한 고민들을 하다 보니, UserManager에 모든 User 로직이 담긴다면 SRP를 결코 지킬 수 없다는 결론에 나왔다.

 

 


 

던져보는 질문

가장 기본으로 돌아가 다음 질문을 던져 보았다. “어떤 이유로 클래스를 수정하게 될까?”

사용자 정보 관리 로직이 바뀐다면? 로그인 정책이 바뀐다면? 비밀번호 복잡도 규칙이 바뀐다면?

모두 변경 이유가 다르지만, 모두 기존 설계대로라면 UserManager를 수정해야 한다.

UserManager라는 큰 틀을 다음과 같이 쪼갠다면 어떨까? Authentication, PasswordService, RoleService. 사용자 정보 정책이 바뀐다면 UserManager를, 인증 방법이 바뀐다면 Authentication을, 비밀번호 정책이 바뀐다면 PasswordService를 바꾸면 될 것이다. 즉, 변경 이유 가 달라지면 수정되어야 하는 클래스 또한 달라지는 것이다.

 

 


 

본격적인 고민

하지만 SRP를 어디까지 적용해야 할까? 라는 질문은 단순히 기능을 쪼개는 것으로 끝나지 않는다. 오히려 애매하게 쪼개면 새로운 문제들이 발생하기 마련이다. 예를 들어 잘게 쪼개어진 서비스간 잦은 의존이 발생한다면 결국 스파게티 코드가 되고 말 것이다.

결국 우리는 단순한 클래스 분리 가 아닌 적절한 경계를 찾아 클래스를 수정해야 할 단 하나의 이유만 존재하게 만들어야 한다는 결론에 다달았다. 그리고 이 단 하나의 이유란 단순히 책임이 아닌 비즈니스 로직 측면에서의 이유가 포함되어야 한다.

 

 

문제 인식

위 UserManager 클래스는 여러 기능이 한곳에 모여 있다.

  1. CRUD : createUser, updateUser, deleteUser
  2. 인증/인가 : login, logout
  3. 비밀번호 : resetPassword

결국 이유는 모두 다르지만 하나의 파일을 수정하여 SRP 를 위배하게 된다.

 

 


 

하나의 이유를 어떻게 찾을까?

클래스를 변경할때, 하나의 이유에 의해서만 변경되어야 한다고 결론지었다. 그렇다면 이 하나의 이유가 무엇인지 구체적으로 발견해야 한다.

  1. 변경이 일어나는 지점을 찾아보자 → 비즈니스 로직에 의거하여 어느 지점에 변경이 있을 것인지 예상해 보자. 자주 바뀔만한 부분이 보인다면 관련 로직은 별도의 클래스로 묶어 두는것이 좋을 것이다.
    1. 예를 들어 사용자 인증 로직이 자주 바뀔것 같다고 가정해 보자. 보안 정책, 혹은 OAuth 연동 방식이 추가로 들어갈 수도 있고, 요구사항이 수시로 변한다고 가정해 보자.
    2. 그렇다면 이 부분만 따로 Authentication 이라는 별도의 클래스로 분리하는 것이 합리적이다.
  2. 서로 성격이 다른 기능을 구별해 보자 → 사용자 정보관리 와 인증/인가 는 본질적으로 다르다. 사용자 정보를 어떻게 저장하고 불러올지, 어떤 권한을 부여해야 할지는 별개의 문제이다.

여기서 중요한 점은 “구분은 하되, 너무 조밀하게 쪼개지 말 것” 이다. 지나치게 여러 파일로 흩어져 있다면 오히려 코드가 꼬일 가능성이 높아진다.

 

 


 

분리 전략: 적절한 경계 찾기

 

인증 / 인가

 

로그인, 로그아웃은 외부 서비스나 보안 정책 변화등에 의해 자주 수정될 가능성이 높다. 즉, 별도의 클래스로 관리하는 것이 합리적인 생각이다.

public class AuthenticationService {
    public void login(String username, String password) {
        // 로그인 로직 (DB 조회, 토큰 발급 등)
    }

    public void logout(String token) {
        // 로그아웃 로직 (세션 만료, 토큰 무효화 등)
    }
}

 

CRUD

 

사용자 정보의 CRUD 는 DB 스키마에 따라 바뀔 수 있다. 즉, DB와 상호작용을 전담하는 Service 클래스를 하나 만들자

public class UserService {
    public void createUser(User user) {
        // 사용자 정보 저장 로직
    }
    public void updateUser(User user) {
        // 사용자 정보 업데이트 로직
    }
    public void deleteUser(Long userId) {
        // 사용자 정보 삭제 로직
    }
    // ...
}

 

비밀번호

 

비밀번호 정책은 종종 강화되거나 변경될 여지가 많은 영역이다. PasswordService 같은 클래스를 두어 비밀번호에 대한 책임을 지게 하자

public class PasswordService {
    public boolean validateComplexity(String password) {
        // 비밀번호 복잡도 검증 (대소문자, 특수문자, 길이 등)
        return true; // 예시
    }

    public void resetPassword(Long userId) {
        // 임시비밀번호 발급, 혹은 새 비밀번호 설정 로직
    }
}


결론

이러한 고민을 할 때, 목적을 항상 잃지 말아야 한다. SRP 로 고민을 시작했지만, 사실 우리의 목적은 SRP가 아닌 코드를 어떻게 하면 깔끔하고 유지보수하기 쉽게 만들것인가 이다.

  • 단일 책임 원칙은 “각 클래스가 오직 하나의 변경 이유만 가지도록 하라”는 것
  • UserManager처럼 방대한 기능을 한 클래스에 몰아넣지 말고, 비즈니스 로직이 바뀌는 이유를 기준으로 클래스를 분리해보면 훨씬 더 깔끔하고 유지보수하기 쉬운 구조를 만들 수 있다
  • 다만 지나친 분리로 인해 오히려 의존 관계가 복잡해지지 않도록 주의해야 한다.

결국 SRP는 마법 같은 절대 기준이 아니라, 적절한 경계를 찾아가는 과정 이라 볼 수 있다.

코드를 짤 때, “이 기능이 자주 바뀔 만한가?”, “비즈니스적으로 별도의 관심사인가?” 등을 고민하면서 클래스를 분리해보는 습관을 들이는 것이 좋겠다.