SOLID
객체 지향 프로그래밍 및 설계의 기본원칙 5가지를 의미한다.
1. SRP(단일 책임원칙)
- 한 클래스는 하나의 책임만 가져야 한다.
- 변경이 필요할 때, 수정할 대상이 1가지만 수정하면 되도록 코드를 작성할 것
@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가 여러 액터로 부터 하나의 책임을 갖고 있지 못하기 때문에 이를 위해 비밀번호 암호화에 대한 책임을 분리해야 합니다.
SimplePasswordEncoder라는 추상화 클래스를 통해서 Userservice를 책임분리를 해야한다.
@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);
}
}
2. OCP(개방-폐쇄 원칙)
- 확장에는 열려있으나 변경에는 닫혀 있어야 한다.
- 확장에는 열려있으나 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
- 변경에는 닫혀 있어야 한다. :기존의 코드를 수정하지 않고 동작을 추가하거나 변경할 수 있다.
@Component
public class SHA256PasswordEncoder {
private final static String SHA_256 = "SHA-256";
public String encryptPassword(final String pw) {
final MessageDigest digest;
try {
digest = MessageDigest.getInstance(SHA_256);
} catch (Exception e) {
throw new Exception();
}
final byte[] encodedHash = digest.digest(pw.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encodedHash);
}
private String bytesToHex(final byte[] encodedHash) {
final StringBuilder hexString = new StringBuilder(2 * encodedHash.length);
for (final byte hash : encodedHash) {
final String hex = Integer.toHexString(0xff & hash);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
암호화 규칙을 새롭게 바꾸려고 했는데 기존의 암호화 정책과 무관한 UserService를 같이 수정해야하는 문제가 발생한다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final SHA256PasswordEncoder passwordEncoder;
...
}
기존의 코드를 수정하지 않으면서 다음에 암호화 정책이 변경될 때 UserService에 변경이 필요해 집니다.
→ 추상화에 의존하도록 만들어야 합니다.
public interface PasswordEncoder {
String encryptPassword(final String pw);
}
@Component
public class SHA256PasswordEncoder implements PasswordEncoder {
@Override
public String encryptPassword(final String pw) {
...
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder 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);
}
}
encryptedPassword를 통해 처리함으로써, 비밀번호 변경방법이 바뀌더라도 User 회원가입 부분에서 코드를 따로 변경할 필요성이 없어지게됩니다. 즉, 클래스 간에 의존성을 낮출 수 있습니다.
3. LSP(리스코프 치환 원칙)
- 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 자식 컴포넌트의 기준을 부모 컴포넌트는 지니고 있어야한다는 원칙
- Rectangle() > Square()
@Getter
@Setter
@AllArgsConstructor
public class Rectangle {
private int width;
private int 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);
}
}
Square는 1개의 변수만을 생성자로 받으며, width나 height 1개 만을 설정하는 경우 모두 설정되도록 메소드가 오버라이딩 되어 있습니다.
이를 이용하는 클라이언트는 당연히 직사각형의 너비와 높이가 다르다고 가정할 것이고, 직사각형을 resize()하기를 원하는 경우 다음과 같은 메소드를 만들어 너비와 높이를 수정할 것입니다. (항상 클라이언트의 입장에서 생각해야 함에 유의해야 합니다.)
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()의 파라미터로 정사각형인 Square이 전달되는 경우입니다. Rectangle은 Square의 부모 클래스이므로 Square 역시 전달이 가능한데, Square는 가로와 세로가 모두 동일하게 설정되므로 예를 들어 다음과 같은 메소드를 호출하면 문제가 발생할 것입니다.
정사각형인데 직사각형이 나왔으니 말이죠
Rectangle rectangle = new Square();
resize(rectangle, 100, 150);
이러한 케이스는 명백히 클라이언트의 관점에서 부모 클래스와 자식 클래스의 행동이 호환되지 않으므로 리스코프 치환 원칙을 위반하는 경우입니다. 리스코프 치환 원칙이 성립한다는 것은 자식 클래스가 부모 클래스 대신 사용될 수 있어야 하기 때문입니다.
리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조합니다. 위의 예시에서 클라이언트는 직사각형의 너비와 높이는 다를 것이라고 가정하는데, 정사각형은 이를 준수하지 못합니다.
여기서 대체 가능성을 결정해야 하는 것은 해당 객체를 이용하는 클라이언트임을 반드시 잊지 말아야 합니다.
이러한 문제를 해결하기 위해 빈 메소드를 호출하도록 하거나 호출 시에 에러를 던지는 등의 조치를 취할 수 있습니다. 하지만 이러한 방법은 클라이언트가 예상하지 못할 수 있으므로 추상화 레벨을 맞춰서 메소드 호출이 불가능하도록 하거나(Square은 resize를 호출하지 못하게 하거나) 해당 추상화 레벨에 맞게 메소드를 오버라이딩 하는게 합리적입니다.
4. 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);
}
}
isCorrectPassword처럼 패스워드를 확인하는 로직이 필요해서 넣었습니다. 하지만 UserService에서는 encryptPassword만 필요하고 isCorrectPassword가 필요하지 않습니다. 이를 해결하기 위해서 비밀번호를 검사를 의미하는 별도의 인터페이스를 만들고 이를 해당 인터페이스로 주입받도록 하는 것이 적합합니다.
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);
}
}
클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있습니다. 그리고 인터페이스를 클라이언트의 기대에 따라 분리하여 변경에 의해 영향을 제어합니다.
하지만 만약 비밀번호를 점검하지 않는다면, 비밀번호를 확인하는 로직을 사용하지 않는 것도 중요합니다.
필요로 하지 않는 인터페이스를 없애서 불필요한 의존성을 줄이는 것입니다. 이 방법을 통해 사용자의 예기치 못하는 에러를 회피할 수 있습니다.
5. DIP(의존 관계 역전 원칙)
- 추상화에 의존해야하고, 구체화에 의존하면 안 됩니다.
- 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야합니다.
참고 사이트 : [OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID - MangKyu's Diary (tistory.com)
'CS > 운영체제' 카테고리의 다른 글
[3강] 프로세스 관리 (1) | 2024.03.03 |
---|---|
멀티 프로세스와 멀티 스레드 (0) | 2024.01.09 |
운영체제 스케줄링(정처기) (0) | 2023.07.05 |
운영체제 개념(정처기) (0) | 2023.07.03 |