SOLID
SOLID란 객체 지향 프로그래밍을 하며 지키야 하는 5가지 원칙으로, SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙) 의 앞글자를 따서 만들어졌습니다.
SOLID 원칙을 철저히 지키면 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 됩니다.
단일 책임 원칙(SRP)
SRP는 하나의 모듈이 하나의 책임을 가져야 한다는 것 입니다. 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 모듈이 변경되는 이유는 하나여야 합니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void addUser(final String email, final String pw) {
final StringBuilder sb = new StringBuilder();
for(byte b : pw.getBytes(StandardCharsets.UTF_8)) {
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
final String encryptedPassword = sb.toString();
final User user = User.builder()
.email(email)
.pw(encryptedPassword).build();
userRepository.save(user);
}
}
위 코드에서는 회원가입 시 비밀번호 암호화 로직을 포함하고 있는 UserService 로직입니다. 하지만 이런 경우 암호화를 변경할 때 암호화에 대해 책임이 없는 UserService를 변경하게 되고, 여러개의 책임을 가지 게 됩니다.
아래와 같이 수정하여 SRP를 지킬 수 있습니다.
@Component
public class SimplePasswordEncoder {
public void encryptPassword(final String pw) {
final StringBuilder sb = new StringBuilder();
for(byte b : pw.getBytes(StandardCharsets.UTF_8)) {
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final SimplePasswordEncoder passwordEncoder;
public void addUser(final String email, final String pw) {
final String encryptedPassword = passwordEncoder.encryptPassword(pw);
final User user = User.builder()
.email(email)
.pw(encryptedPassword).build();
userRepository.save(user);
}
}
개방 폐쇄 원칙(OCP)
개방 폐쇄 원칙은 확장에 열려있고, 수정에 닫혀있어야 한다는 원칙입니다.
확장에 열려있다: 요구사항이 변경될 때 새로운 동작을 추가해 애플리케이션의 기능을 확장할 수 있다.
수정에 닫혀있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
바로 위의 코드에서 살펴볼 수 있는데, 비밀번호 암호화 방식을 일괄적으로 변경할 때 SimplePasswordEncoder를 사용하는 모든 코드를 수정해줘야 하는 문제가 있습니다.
하지만 클래스를 바로 사용하는게 아닌, 추상화를 통해 상황에 맞게 갈아 낄 수 있으면 문제가 해결됩니다.
즉, 변하지 않는 부분은 고정하고 변하는 부분을 생략해 추상화함으로써 변경이 필요한 경우에 생략된 부분을 수정하여 개방-폐쇄 원칙을 지킬 수 있습니다.
인터페이스 분리 원칙(ISP)
객체가 충분히 높은 응집도의 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리하는 것을 인터페이스 분리 원칙이라고 합니다.
클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이며, 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메시지)만을 접근하여 불필요한 간섭을 최소화할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있습니다.
@Component
public class SHA256PasswordEncoder implements PasswordEncoder {
@Override
public String encryptPassword(final String pw) {
...
}
public String isCorrectPassword(final String rawPw, final String pw) {
final String encryptedPw = encryptPassword(rawPw);
return encryptedPw.equals(pw);
}
}
위와 같이 정확한 비밀번호인지를 검사하는 로직을 추가하면 UserService에서는 필요없는 isCorrectPassword를 알게 됩니다. 물론 PasswordEncoder에 isCorrectPassword 퍼블릭 인터페이스를 추가해줄 수 있지만, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공한다는 인터페이스 분리 원칙을 지키기 위해 이미 만든 인터페이스를 건드리지 않고 아래와 같이 별도의 인터페이스(PasswordChecker)를 만들고, 해당 인터페이스로 주입받도록 하는게 적절합니다.
public interface PasswordChecker {
String isCorrectPassword(final String rawPw, final String pw);
}
@Component
public class SHA256PasswordEncoder implements PasswordEncoder, PasswordChecker {
@Override
public String encryptPassword(final String pw) {
...
}
@Override
public String isCorrectPassword(final String rawPw, final String pw) {
final String encryptedPw = encryptPassword(rawPw);
return encryptedPw.equals(pw);
}
}
이렇게 분리하여 변경에 대한 영향을 제어하고, 인터페이스를 클라이언트의 기대에 따라 분리해 변경에 의한 영향을 제어하는것을 인터페이스 분리 원칙이라고 부릅니다.
리스코프 치환 원칙(LSP)
하위 타입은 상위 타입을 대체할 수 있어야 한다는 것 입니다.
해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타읍올 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 합니다.
@Getter
@Setter
@AllArgsConstructor
public class Rectangle {
private int width, height;
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
public Square(int size) {
super(size, size);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
public void resize(Rectangle rectangle, int width, int height) {
rectangle.setWidth(width);
rectangle.setHeight(height);
if (rectangle.getWidth() != width && rectangle.getHeight() != height) {
throw new IllegalStateException();
}
}
이렇게 resize를 통해 width와 height를 변경한다고 했을 때, 다음과 같은 메소드를 만드는데 resize의 파라미터로 정사각형인 Square가 전달되는 경우, Rectangle은 Square의 부모 클래스이므로 Square 역시 전달이 가능합니다.
Square는 가로 세로가 모두 동일하게 설정되므로 아래와 같은 메소드를 호출하면 문제가 발생하게 됩니다.
Rectangle rectangle = new Square();
resize(rectangle, 100, 150);
이런 케이스는 부모 클래스와 자식 클래스의 행동이 호환되지 않으므로 리스코프 치환 원칙을 위반하는 경우입니다. 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스의 대한 클라이언트의 가정을 준수해야 합니다.
이런 문제를 해결하기 위해, 빈 메소드를 호출하거나 호출 시 에러를 던지는 등 조치를 취할 수 있지만, 이런 방법은 클라이언트가 예상하지 못할 수 있으므로 추상화 레벨을 맞춰 메소드 호출이 불가능하도록 하거나(Square는 resize 호출하지 못하도록) 해당 추상화 레벨에 맞게 메소드를 오버라이딩 하는게 합리적입니다.
의존 역전 원칙(DIP)
고수준 모듈은 저수준 모듈에 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것입니다.
객체 지향 프로그래밍에서는 객체들 사이에 메시지를 주고 받기 위해 의존성이 생기는데, 의존성 역전의 원칙은 올바른 의존 관계를 설정하기 위한 원칙입니다.
- 고수준 모듈: 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈
- 저수준 모듈: 입력과 출력으로부터 가까운(HTTP, 데이터베이스, 캐시 등과 관련된) 구현 모듈
결국 비즈니스와 관련된 부분이 세부 사항에는 의존하지 않는 설계 원칙을 의미합니다.
위에서 언급한 SImplePasswordEncoder는 변하기 쉬운 암호화 알고리즘과 관련된 구체 클래스인데, UserService가 SimplePasswordEncoder에 직접 의존하는 것은 DIP에 위배되는 것 입니다. 그러므로 UserService가 변하지 않는 추상화에 의존하도록 변경이 필요하고, PasswordEncoder 인터페이스를 만들어 이에 의존하도록 변경하게 되면 암호화 정책이 변경되더라도, 다른곳들로 변경이 전파되지 않으며 유연한 애플리케이션이 됩니다.
의존 역전 원칙은 개방-폐쇄 원칙과 밀접한 관련이 있으며, 의존 역전 원칙이 위배되면 개방-폐쇄 원칙 또한 위배될 가능성이 높습니다. 주의해야 할 점은 의존 역전 원칙에서 의존성이 역전되는 시점은 컴파일 시점이라는 것 입니다. 런타임 시점에서는 UserService는 구체 클래스에 의존합니다. 하지만 의존 역전 원칙은 컴파일 시점 또는 소스 코드 단계에서 의존성이 역전되는 것을 의미하며, 코드에서 UserService는 PasswordEncoder에 의존합니다.(런타임 시점에서 생각하면 X)
SOLID의 핵심은 추상화와 다형성입니다. 구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 유연하고 확장 가늫안 애플리케이션을 만들 수 있습니다.
'Study' 카테고리의 다른 글
캐시 버스팅(Cache busting)이란? (0) | 2023.04.24 |
---|---|
[Jenkins] 젠킨스 설치 중 에러와 해결방법 (0) | 2022.04.29 |
[Jenkins] 젠킨스 플러그인 오프라인 설치 (윈도우 plugins 폴더 위치) (0) | 2022.03.22 |
[AWS 컨퍼런스– 에센셜 클라우드 기초의 모든 것] (0) | 2022.02.21 |