이번 포스팅에서는 객체 지향 프로그래밍의 원칙인 SOLID 중 'S'를 담당하는 Single Responsibility Principle(SRP)에 대해 알아보고자 한다.
SRP
SRP는 단일 책임의 원칙으로서 하나의 클래스는 하나의 책임을 가지도록 해야 한다는 것
이 핵심이다.
책임
책임이라는 것부터 이해해보자. 책임은 역할이라고 정의할 수 있을 것이다. 이해를 돕기 위해 하나의 예시를 들어보겠다.
어느 대장장이는 전투에 필요한 모든 장비와 장신구를 만들 줄 안다. 이 대장장이에게 제작부터 수리까지 맡기면 대장장이는 제대로된 작업이 힘들 것이다. 이를 보완하고자 투구를 제작하는 대장장이, 갑옷을 제작하는 대장장이, 무기를 제작하는 대장장이를 영입해 각각 맡은 것만 책임지기로 했다.
그 후로는 제작한 것들을 유지보수하기도 쉬워지게 되었다.
하나의 클래스가 모든 책임을 다 가지고 있어도 되지만, 한 대장장이에게 모든 것을 맡기면 효율이 안 나오는 것처럼 클래스도 그 클래스가 수행해야 되는 책임에 대해서만 맡고 있는 것이 유지보수 측면에서 훨씬 용이하다.
책임을 명확히 하기
객체 지향 프로그래밍을 할 때, SRP를 준수해야 된다는 것에 대해 이해를 했을 것이다. 서버에서 API를 작성할 때, 각 계층은 자신들의 역할이 있다. service 계층은 요청에 대한 동작을 수행하는 곳이다. 예를 들어, 사용자를 생성하는 API(registerMember)가 있다고 할 때, registerMember()는 사용자를 생성하는 책임만을 가져야 한다. 다른 책임을 가지고 있으면 SRP의 위배되는 것이다.
@Getter
public class MemberRequest {
private String username;
private String email;
private String password;
}
// registerMember가 member를 생성하는 책임만 가지는 것이 아니라 password를 encode하는 책임도 가지고 있다.
private final PasswordEncoder passwordEncoder;
public MemberResponse registerMember(MemberRequest memberRequest) {
String encodedPassword = passwordEncoder.encode(memberRequest.getPassword());
Member member = new Member(
memberRequest.getEmail(),
memberRequest.getUsername(),
encodedPassword
);
memberRepository.save(member);
return MemberReponse.from(member);
위의 코드를 보면, PasswordEncoder를 통해 registerMember() 내에서 password를 encode하는 것을 확인할 수 있다. registerMember는 member 객체를 생성하는 책임만을 가지면 된다. encode를 하는 것은 request에게 맡기면 된다는 것이다. 그럼 위 코드에서 SRP를 준수한 코드는 아래와 같다.
@Getter
public class MemberRequest {
private String username;
private String email;
private String password;
public void validate() {
if (username == null || username.trim().isEmpty()) {
throw new RuntimeException("Invalid username");
}
if (email == null || email.trim().isEmpty() || !email.contains("@")) {
throw new RuntimeException("Invalid email");
}
if (password == null || password.trim().isEmpty() || password.length() < 4) {
throw new RuntimeException("Invalid password");
}
}
public String getEncodedPassword(PasswordEncoder passwordEncoder) {
return passwordEncoder.encode(password);
}
}
private final PasswordEncoder passwordEncoder;
public MemberResponse registerMember(MemberRequest memberRequest) {
String encodedPassword = memberRequest.getEncodedPassword(passwordEncoder);
Member member = new Member(
memberRequest.getEmail(),
memberRequest.getUsername(),
encodedPassword
);
memberRepository.save(member);
return MemberReponse.from(member);
registerMember() 는 member 객체를 생성하는 것에만 책임을 다하면 되는 것이다. 다시 말해, 요청받은 데이터(request)의 유효성을 어떻게 검증하든 어떻게 바꾸든하는 것들은 신경쓰지 않는다는 것이다. 이것은 순수 request의 책임이기 때문이다.
이렇게 책임을 분리함으로써, 추후에 복잡한 로직이 추가가 되어도 registerMember()는 아무것도 신경쓰지 않아도 된다. MemberRequest가 알아서 할 것이기 때문이다. 이로써 유연성, 확장성, 유지보수성이 높아지는 것이다.
그런데 어찌보면 위의 예시는 되게 간단한 역할인데 이런 것까지 굳이 책임 분리해야 하나 싶을 수 있다.
사실 필자도 그렇다 생각한다. 하지만, 큰 규모의 서비스를 만들어야 할 때에는 이러한 책임 분리가 확실한 도움이 될 것이기에 지금은 그러한 때를 대비한 훈련이라고 생각해보자.
'TIL' 카테고리의 다른 글
의존성과 의존성 역전 이해하기 (3) | 2024.11.01 |
---|---|
순환 참조 이해해보기 (0) | 2024.10.31 |
함께 자라기 - 애자일로 가는 길 (2) | 2024.08.30 |
즉시로딩 & 지연로딩 (0) | 2024.07.09 |
Spring Batch란? (0) | 2024.07.01 |