🤚 접근 제한자 (Access Modifier)
클래스의 멤버들은 객체 자신들만의 속성이자 특징이기 때문에 대외적으로 공개되는 것이 결코 좋은 것은 아니다
따라서 프로그래머는 접근 제한자를 통해 객체의 멤버들에게 접근을 제한할 수 있다
접근 제한자 종류
1. public
- package, class가 동일하지 않아도 모든 접근이 가능한 제한자
- 같은 클래스, 같은 패키지, 다른 패키지의 다른 클래스 내에서 허용
2. protected
- 같은 package에서만 접근을 허용하고 다른 package에서 접근하려면 해당 class를 상속받을 시에만 접근이 가능한 제한자
- 같은 class에서 접근 허용
- 같은 package의 다른 class에서 접근 허용
- 다른 package의 상속받은 class에서 접근 허용
- 다른 package의 다른 class에서 접근 불가
3. default
- 동일 package에서만 접근을 허용하는 제한자
- 접근 제한자가 생략된 경우, 기본적으로 default 접근 제한자가 적용된다
- default는 자동으로 선언되기 때문에 변수나 메소드 앞에 명시적으로 적으면 안된다
- 같은 class 내에서 허용
- 같은 package 내의 다른 class에서 허용
- 다른 package에서는 접근 불가
4. private
- 동일 package, 다른 package 모두 접근이 불가능하고, 같은 class 내에서만 접근을 허용하는 제한자
- 같은 java 파일이더라도 class가 다른 경우 접근이 불가능하다!
접근 제한자 별 사용 가능 범위
- Class에 사용 가능 : public, default
- Constructor(생성자) : public, protected, default, private
- Member 변수 : public, protected, default, private
- Member 메소드 : public, protected, default, private
- 지역 변수 : 접근 제한자 사용 불가
캡슐화
- 정보 은닉과 같은 재질(?)
- 관련이 있는 데이터와 동작들을 하나로 묶어 요약하고, 사용자에게는 내부적인 접근을 허용하지 않는 대신 사용의 편의성을 제공한다
class Variable {
private int a; // 은닉화
// 캡슐화
public void setA(int a) {
this. a = a;
}
public int getA() {
return a;
}
}
public class MethodTest {
pubilc static void main(String[] args) {
Variable v = new Variable();
// System.out.println(v.a); // 호출 불가
v.setA(100);
System.out.println(v.getA());
}
}
캡슐화의 장점
1. 데이터 보호
- 은닉화를 통해 데이터의 접근을 제어할 수 있다
2. 유지보수
- 각 기능을 하나의 모듈처럼 활요앟여 객체간의 이식성이 높고 독립적인 면을 유지할 수 있다
3. 사용자 편의성
- 사용자는 메소드의 원리와 과정등의 내부 동작을 생각할 필요 없이 사용법만 알면 쉽게 사용할 수 있다
생성자(Constructor)
객체가 생성될 때 객체의 초기화를 위해 실행되는 메소드
생성자 규칙
- 클래스명과 메서드명이 동일해야 한다
- 리턴 타입이 없어야 한다 (void도 안됨!)
생성자 특징
- 생성자는 반환값이 없지만, 반환 타입을 void 형으로 선언하지 않는다
- 생성자는 초기화를 위한 데이터를 인수로 전달받을 수 있다
- 객체를 초기화하는 방법이 여러 개 존재할 경우, 하나의 클래스가 여러 개의 생성자를 가질 수 있다
- 즉, 생성자도 하나의 메소드이므로, 메소드 오버로딩이 가능하다!
생성자 선언 방법
public 클래스(매개변수) {
...
}
생성자를 선언하는 방법은 위와 같이 하면 된다
이 때 [클래스] 부분은 클래스의 이름과 동일하게 적어줘야 한다
[매개변수]에는 필수는 아니며 필요한 겨웅 매개변수를 전달받아 인스턴스 변수를 초기화할 수 있다
또한 위처럼 클래스의 생성자는 어떠한 반환값도 명시하지 않는다!
생성자의 호출
자바에서는 new 키워드를 사용하여 객체를 생성할 때 자동으로 생성자가 호출된다
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
public class Method02 {
public static void main(String[] args) {
Car myCar = new Car("아반떼", 2016, "흰색", 200); // 생성자의 호출
System.out.println(myCar.getModel()); // 생성자에 의해 초기화되었는지를 확인함.
}
}
기본 생성자(default constructor)
자바의 모든 클래스에는 하나 이상의 생성자가 정의되어 있어야 한다
하지만 특별히 생성자를 정의하지 않고도 인스턴스를 생성할 수 있다
이는 자바 컴파일러가 기본 생성자(default constructor)를 기본적으로 제공해주기 때문이다
기본 생성자는 매개변수를 하나도 갖지 않으며, 아무런 명령어도 포함하고 있지 않는다
자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않으면, 자동으로 다음과 같은 기본 생성자를 추가한다
// 클래스명() {}
Car() {} // Car 클래스의 default constructor
위와 같이 기본 생성자는 어떠한 매개변수도 전달받지 않으며, 기본적으로 아무런 동작도 하지 않는다
기본 생성자 예시)
class Car {
private String modelName = "소나타";
private int modelYear = 2016;
private String color = "파란색";
public String getModel() {
return this.modelYear + "년식 " + this.color + " " + this.modelName;
}
}
public class Method03 {
public static void main(String[] args) {
Car myCar = new Car(); // 기본 생성자의 호출
System.out.println(myCar.getModel()); // 2016년식 파란색 소나타
}
}
위의 예제에서 Car 클래스의 인스턴스인 myCar는 기본 생성자를 사용하여 생성된다
그러나 기본 생성자는 아무런 동작도 하지 않으므로, 인스턴스 변수를 클래스 필드에서 바로 초기화한다
이처럼 인스턴스 변수의 초기화는 생성자를 사용하여 수행할 수도 있지만, 클래스 필드에서 바로 수행할 수도 있다
하지만 만약 매개변수를 갖는 생성자를 하나라도 정의했다면, 기본 생성자는 자동으로 추가되지 않는다
따라서 매개변수를 갖는 생성자를 하나 이상 정의한 후, 기본 생성자를 호출하면 오류가 발생하게 된다
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
① Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
public class Method04 {
public static void main(String[] args) {
② Car myCar = new Car(); // 기본 생성자의 호출
③ // Car myCar = new Car("아반떼", 2016, "흰색", 200); // 생성자의 호출
System.out.println(myCar.getModel()); // 생성자에 의해 초기화되었는지를 확인함.
}
}
위의 예제는 ①번 라인에서 4개의 매개변수를 갖는 생성자를 정의하고 있기 때문에, 자바 컴파일러는 Car 클래스에 별도의 기본 생성자를 추가하지 않는다
하지만 ②번 라인에서는 기본 생성자를 호출하여 인스턴스를 생성하려고 하고 있다
따라서 자바 컴파일러는 오류를 발생시킬 것이며, ③번 라인과 같이 4개의 매개변수를 전달해야만 인스턴스가 생성된다
생성자 오버로딩
생성자도 메소드의 일종이기 때문에, 하나의 클래스에 여러 개의 입력 항목이 다른 생성자를 만들 수 있다
이는 메소드 오버로딩과 동일한 개념이다!
public class Car {
public Car(String name) {
...
}
public Car(int cost) {
...
}
}
SOLID
SOLID란 객체 지향 프로그래밍을함녀서 지켜야 하는 5대 원칙으로 각각 SRP, OCP, LSP, DIP, ISP의 앞글자를 따서 만들어졌다
SOLID 원칙을 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 되는 것으로 알려져있다
https://mangkyu.tistory.com/194
1. SRP(Single Reponsibility Principle) - 단일 책임의 원칙
- 하나의 모듈은 한 가지 책임을 가져야 한다
- 이는 모듈이 변경되는 이유가 한가지여야 된다는 것을 의미한다
- 이 때 '변경의 이유가 한 가지'라는 것은 해당 모듈이 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야한다는 것을 의미한다
- 만약 어떤 모듈이 여러 액터에 대해 책임을 갖는다면, 여러 액터들로부터 변경에 대한 요구가 올 수 있으므로, 해당 모듈을 수정해야 하는 이유 역시 여러 개가 될 수 있다
- 반면, 어떤 클래스가 단 하나의 책임만을 갖는다면 특정 액터로부터 변경을 특정할 수 있으므로 해당 클래스를 변경해야 하는 이유와 시점이 명확해진다
[ 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의 사용자 추가 로직(addUser)에는 아래와 같이 다양한 액터로부터 변경이 발생할 수 있다
- 기획팀 : 사용자를 추가할 때 역할(Role)에 대한 정의가 필요하다
- 보안팀 : 사용자의 비밀번호 암호화 방식에 개선이 필요하다
- etc
이는 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);
}
}
위와 같이 비밀번호 암호화를 책임지는 별도의 클래스(SimplePasswordEncoder)를 만들어 UserService로부터 이를 추상화하고, 해당 클래스를 합성하여 접근 및 사용하면 위의 문제를 해결할 수 있다
따라서 위처럼 SRP(단일 책임 원칙)을 지키면 변경이 필요할 때 수정할 대상이 명확해진다
그리고 SRP의 장점은 시스템이 커질수록 극대화되는데, 시스템이 커지면서 서로 많은 의존성을 갖게되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다!
단일 책임 원칙을 적용하여 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 추상화함으로써 애플리케이션의 변화에 손쉽게 대응할 수 있다
2. OCP(Open-Closed Principle) - 개방 폐쇄 원칙
확장에 대해 열려있고, 수정에 대해서는 닫혀있어야 한다
'확장에 대해 열려있다' -> 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다는 의미이다
'수정에 대해서 닫혀있다' -> 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다
[OCP 예시]
이번에는 위의 예시에서 비밀번호 암호화를 강화해야 한다는 요구사항이 새롭게 들어왔다고 가정하자
비밀번호 암호화를 강화하기 위해 아래와 같이 SHA-256 알고리즘을 사용하는 PasswordEncoder를 생성하였다
@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 (NoSuchAlgorithmException e) {
throw new IllegalArgumentException();
}
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;
...
}
이는 기존의 코드를 수정하지 않아야 하는 개방 폐쇄 원칙(OCP)에 위배된다
그리고 나중에 또 다시 비밀번호 암호화 정책을 변경해야 한다는 요구사항이 온다면 또 다시 UserService에 변경에 필요해진다
이러한 문제를 해결하고 OCP를 지키기 위해서는 추상화에 의존해야 한다
추상화란 핵심적인 부분만 남기고 불필요한 부분은 제거함으로써 복잡한 것을 간단히 하는 것이고 , 추상화를 통해 변하지 않는 부분만 남김으로써 기능을 구체화하고 확장할 수 있다
변하지 않는 부분은 고정하고 변하는 부분을 생략하여 추상화함으로써 변경이 필요한 경우에 생략된 부분을 수정하여 OCP를 지킬 수 있다
위의 예제에서 변하지 않는 것은 사용자를 추가할 때 암호화가 필요하다는 것이고, 변하는 것은 사용되는 구체적인 암호화 정책이다
그러므로 UserSerivce는 어떠한 구체적인 암호화 정책이 사용되는지는 알 필요 없이 단지 passwordEncoder 객체를 통해 암호화가 된 비밀번호를 받기만 하면 된다
그러므로 UserService가 구체적인 암호화 클래스에 의존하지 않고 PasswordEncoder라는 인터페이스에 의존하도록 추상화하면 OCP를 만족하는 코드를 작성할 수 있다
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);
}
}
OCP가 본질적으로 얘기하고자 하는 것은 추상화이며, 이는 결국 런타임 의존성과 컴파일 타임 의존성에 대한 이야기이다
여기서 런타임 의존성이란 애플리케이션 실행 시점에서의 객체들의 관계를 의미하고, 컴파일 타임 의존성이란 코드에 표현된 클래스들의 관계를 의미한다
다형성을 지원하는 객체지향 프로그래밍에서 런타임 의존성과 컴파일 타임 의존성은 동일하지 않다
위의 예제에서 UserService는 컴파일 시점에 추상화된 PasswordEncoder에 의존하고 있지만 런타임 시점에는 구체 클래스인 SHA256PasswordEncoder에 의존하고 있는 것이다
객체가 알아야 하는 지식이 많으면 결합도가 높아지고, 결합도가 높아질수록 OCP를 따르는 구조를 설계하기 어려워진다
추상화를 통해 변하는 것들은 숨기고 변하지 않는 것들에 의존하게 하면 우리는 기존의 코드 및 클래스들을 수정하지 않은 채로 애플리케이션을 확장할 수 있다
또한 이것이 OCP가 의미하는 것이다
3. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
객체가 충분히 높은 응집도의 작은 다위로 설계되었더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해줄 필요가 있다는 것이다
즉, ISP란 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이다
인터페이스 분리 원칙을 준수함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메세지)만을 접근하여 불필요한 간섭을 최소화할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다
ISP를 지킨다는 것은 어떤 구현체에 부가 기능이 필요하다면 이 인터페이스를 구현하는 다른 인터페이스를 만들어서 해결할 수 있다
예를 들어 파일 읽기/쓰기 기능을 갖는 구현 클래스가 있는데 어떤 클라이언트는 읽기 작업만을 필요로 한다면 별도의 읽기 인터페이스를 만들어 제공해주는 것이다
[ISP 예시]
위의 예시에서 사용자가 비밀번호를 변경할 때 입력한 비밀번호가 기존의 비밀번호와 동일한지 검사해야 하는 로직을 다른 Authentication 로직에 추가해야 한다고 가정하자
그러면 우리는 다음과 같은 isCorrectPassword라는 퍼블릭 인터페이스를SHA256PasswordEncoder를 추가해줄 것이다
@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에서 비밀번호 암호화를 위한 encryptPassword()만을 필요로 하고, 불필요하게 isCorrectPassword를 알 필요가 없다
현재 UserService는 PasswordEncoder를 주입받아 encryptPassword에만 접근 가능하므로 인터페이스 분리가 잘 된 것처럼 보인다
하지만 새롭게 추가될 Authentication 로직에서는 isCorrectPassword에 접근하기 위해 구체 클래스인 SHA256PasswordEncoder를 주입받아야 하는데 그러면 불필요한 encryptPassword에도 접근 가능해지고, 인터페이스 분리 원칙을 위배하게 된다
물론 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);
}
}
위처럼 클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다
그리고 이렇게 인터페이스를 클라이언트의 기대에 따라 분리하여 변경에 의한 영향을 제어하는 것을 ISP라고 부른다!
4. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
LSP는 1988년 바바라 리스코프가 올바른 상속 관계의 특징을 정의하기 위해 발표한 원칙이다
하위 타입은 상위 타입을 대체할 수 있어야 한다는 것이다
즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다
[LSP 예시]
이번에는 직사각형(Rectangle)과 정사각형(Square)을 예시로 들어보자
@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);
}
}
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);
이러한 케이스는 명백히 클라이언트의 관점에서 부모 클래스와 자식 클래스의 행동이 호환되지 않으므로 리스코프 치환 원칙을 위반하는 경우이다
LSP가 성립한다는 것은 자식 클래스가 부모 클래스 대신 사용될 수 있어야 하기 때문이다
LSP는 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조한다
위의 예시에서 크라이언트는 직사각형의 너비와 높이는 다를 것이라고 가정하는데, 정사각형은 이를 준수하지 못한다
우리는 여기서 대체 가능성을 결정해야 하는 것은 해당 객체를 이용하는 클라이언트임을 반드시 잊지 말아야 한다
이러한 문제를 해결하기 위해 빈 메소드를 호출하도록 하거나 호출 시에 에러를 던지는 등의 조치를 취할 수 있다
하지만 이는 클라이언트가 예상하지 못할 수 있으므로 추상화 레벨을 맞춰서 메소드 호출이 불가능하도록(Square는 resize 호출 X) 하거나, 해당 추상화 레벨에 맞게 메소드를 오버라이딩 하는 것이 합리적이다!
5. DIP (Dependency Inversion Principle) - 의존 역전 원칙
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다
객체지향 프로그래밍에서는 객체들 사이에 메세지를 주고받기 위해 의존성이 생기는데, 의존성 역전의 원칙은 올바른 의존 관계를 위한 원칙에 해당된다
고수준 모듈 : 변경이 없는 추상화 된 클래스 (또는 인터페이스)
저수준 모듈 : 변하기 쉬운 구체 클래스
DIP란 결국 추상화에 의존하며 구체화에는 의존하지 않는 설계 원칙을 의미한다
우리는 위의 예시들을 살펴보면서 DIP에 준수하도록 코드를 수정한 경험이 있다
위에서 봤던 SimplePasswordEncoder는 변하기 쉬운 구체 클래스인데, UserService가 SimplePasswordEncoder에 직접 의존하는 것은 의존성 역전 원칙에 위배되는 것이다
그러므로 UserService가 변하지 않는 추상화에 의존하도록 변경이 필요하고, 우리는 PasswordEncoder 인터페이스를 만들어 이에 의존하도록 변경하였다
UserService가 추상화 된 PasswordEncoder에 의존하므로 비밀번호 암호화 정책이 변경되어도 다른 곳들로 변경이 전파되지 않으며 유연한 애플리케이션이 된다
실제 예시에서 살펴보았듯이 DIP는 개방 폐쇄 원칙과 밀접한 관련이 있으며, DIP에 위배되면 OCP(개방 폐쇄 원칙) 역시 위배되게 될 가능성이 높다
또한 DIP에서 주의해야 하는 것이 있는데, DIP에서 의존성이 역전되는 시점은 컴파일 시점이라는 것이다!
런타임 시점에는 UserService가 SHA256PasswordEncoder라는 구체 클래스에 의존하게 된다
하지만 DIP는 컴파일 시점 또는 소스 코드 단계에서의 의존성이 역전되는 것을 의미하며, 코드 상에서는 UserService가 PasswordEncoder라는 인터페이스에 의존한다
위의 내용을 보고나면 객체 지향 설계 원칙인 SOLID가 얘기하는 핵심은 결국 추상화이다!
구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 우리는 유연하고 확장 가능한 애플리케이션을 만들 수 있게 된다!
참고)
https://velog.io/@gillog/Java-%EC%A0%91%EA%B7%BC-%EC%A0%9C%ED%95%9C%EC%9E%90
https://gyrfalcon.tistory.com/entry/JAVA-%EC%A0%91%EA%B7%BC-%EC%A0%9C%ED%95%9C%EC%9E%90
http://www.tcpschool.com/java/java_methodConstructor_constructor
https://mangkyu.tistory.com/194
'야미스터디 > Java' 카테고리의 다른 글
[Java] 오버라이딩 vs 오버로딩 📌 (0) | 2022.08.17 |
---|---|
[Java] Mutable vs Immutable 📌 (0) | 2022.08.07 |
[JAVA] static 변수 (0) | 2022.07.26 |
[Java] 객체지향 vs 절차지향 📌 (0) | 2022.07.22 |
[JAVA] RuntimeException (0) | 2022.07.21 |
댓글