Java

[객체지향설계원칙] SRP

shininghyunho 2024. 5. 14. 17:58

객체지향(OOP)를 잘 설계하기 위해서는 5가지 원칙이 있습니다.

원칙은 말 그대로 지켜야하는 기조입니다.

 

이 원칙들은 코드가 복잡해지고 문제가 생겼을때 빛을 발휘합니다.

그냥 무작정 외우는것보단 이해해야 '이걸 이렇게하는게 맞나?' 에 대한 근거가 될것입니다.

 

SRP

Single Responsibility Principle. 단일 책임 원칙입니다.

OCP와 더불어 OOP의 기반이 되는 원칙입니다.

 

클래스는 하나의 책임을 져야한다.

= 클래스를 변경하는 이유한가지여야한다.

되게 모호하고 포괄적인 원칙입니다.

 

쉽게 생각해서,

클래스가 커졌는데 책임을 하나만 지고있나?

책임을 분리해서 재설계 해볼수 있지 않을까?

에 대한 답변이라고 볼 수 있습니다.

 

예를들어

매번 프로젝트마다 등장하는 유저의 로그인 기능이 있습니다.

유저 회원가입, 유저 로그인 기능을 구현하면 다음과 같습니다.

 

// UserService: 유저 생성 및 로그인 담당
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String username, String password) {
        userRepository.save(new User(username, hashPassword(password)));
    }

    public boolean login(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return false;
        }

        // 입력 비밀번호 해쉬 후 비교
        String hashedSavedPassword=user.getHashedPassword;
        String hashedInputPassword=hashPassword(password);
        return hashedSavedPassword.equals(hashedInputPassword);
    }

    // 비밀번호 해쉬 함수 (예시: SHA-256)
    public String generateHash(String input) {
        // ... (UserService의 hashPassword 함수와 동일)
    }
}

 

하나의 클래스에서 유저 생성, 로그인, 인증기능을 같이하고 있습니다.

 

이렇게 구현하면 문제점이 나타납니다.

  1. 클래스가 크고 이름이 모호해 기능을 파악하기가 힘들다.
  2. 하나의 기능을 바꾸고자하면 전체 클래스가 변경된다.

UserService라는 이름에서 '유저에 생명주기를 다루겠구나'라고 생각하는데

막상 열어보면 여러가지 기능이 있어 기능 파악이 난감합니다.

 

두번째 문제는 더욱 심각한데,

유지보수의 문제가 있기 때문입니다.

만약에 로그인할때 비밀번호뿐만 아니라 IP도 비교한다고 해보죠.

그러면 UserService 클래스와 login 메서드, hashPassword 메서드 까지 모두 변경대상입니다.

변경 대상은 한가지인데 굉장히 여러군데를 수정해야합니다.

 

그래서 책임을 분리하여 재설계가 필요합니다.

// UserService: 유저 생성만 담당
public class UserService {
    private final UserRepository userRepository;
    private final AuthHandler authHandler;

    public UserService(UserRepository userRepository, AuthHandler authHandler) {
        this.userRepository = userRepository;
        this.authHandler = authHandler;
    }

    public void createUser(String username, String password) {
        User user = new User(username, authHandler.generateHash(password));
        userRepository.save(user);
    }
}

// LoginService: 로그인 담당
public class LoginService {
    private final UserRepository userRepository;
    private final AuthHandler authHandler;

    public LoginService(UserRepository userRepository, AuthHandler authHandler) {
        this.userRepository = userRepository;
        this.authHandler = authHandler;
    }

    public boolean login(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return false;
        }

        // AuthService를 통해 비밀번호 일치 여부 확인
        return authHandler.authenticate(user.hashedPassword, password);
    }
}

// AuthHandler: 인증 담당 (해쉬된 비밀번호 비교)
public class AuthHandler {
    public boolean authenticate(String hashed, String input) {
        String hashedInput = generateHash(input); // 해쉬된 인풋
        return hashedInput.equals(hashed); // 해쉬된 인풋과 비교
    }

    // 비밀번호 해쉬 함수 (예시: SHA-256)
    public String generateHash(String input) {
        // ... (UserService의 hashPassword 함수와 동일)
    }
}

 

다음과 같이 리팩토링해봅시다.

 

  1. 유저 생성과 로그인 클래스 분리
  2. 공통으로 사용되는 인증기능 클래스 분리

클래스가 분리되어 SRP 원칙을 지킬 수 있게되었습니다.

이제 UserService에서는 유저 생성만

LoginService에서는 로그인 기능만을 담당하게됐습니다.

IP를 추가해서 비교한다면 LoginService만 수정해도 되겠죠.

 

단 설계가 복잡해지기 때문에 전체적인 시스템을 파악하기는 힘들어졌습니다.

실제로 SRP는 정말 하나하나 다 지키긴 힘듭니다.

그랬다가는 수많은 메서드와 클래스가 생겨나 개발자가 파악하기도 힘들고

많이 생겨난 클래스만큼 성능상 저하가 발생하기 때문입니다.

 

그래서 SRP를 준수하되 프로젝트의 규모와 클래스의 규모에 맞게 사용해야합니다.