꼭 객체지향이 아니라 보편적으로 좋은 프로그래밍 코드란 변경이 용이하고, 유지보수와 확장이 쉬운 코드
모든 변수, 함수, 클래스는 목적에 맞게 활용되어야한다는 원칙
- 반복되는 목적의 작업은 하나의 함수나 클래스로 묶어야하고
- DRY, Don’t Repeat Yourself : 반복하지 마라
- 동일한 작업을 수행하는 로직이 여러 곳에 있지 않을 것
- (나쁜 사례) 해당 로직 수정 시 모든 곳에서 수정 필요
- 목적이 여러 개여서는 안되고
- KISS, Keep It Simple and Stupid : 단일 목적을 가져야한다 + 바보가 봐도 이해가능해야한다
- 하나의 함수는 하나의 역할만
- (나쁜 사례) ListItem 을 생성하고 + 로컬 스토리지에 저장
- 목적이 미래를 위한 것이어서도 안된다
- YAGNI, You Ain’t Gonna Need It : 지금 필요한것만 만든다
- 나중에 사용할 기능까지 미리 만들지말 것
- (나쁜 사례) 쓸것으로 생각되서 만들었으나 쓰지 않는 API
다른 상황에 맞게 사용될 수 있는 문제들을 해결하는데에 쓰이는 템플릿
“탑에 다리우스가 왔다면, 가장 먼저 Q 를 피하기 위해
이속과 물방이 중요하니 판금장화부터 올리세요.”
High Cohesion, Loose Coupling을 목표
= SOLID 를 다 읽고 이해하면 알겠지만 결국 추상화와 다형성을 얘기하는 것

하나의 모듈(한 클래스 or 메소드)은 하나의 책임/역할만

[단순한 코드]
public class Ramen {
public void make() {
System.out.println("물 끓이기");
System.out.println("스프 넣기");
System.out.println("면 넣기");
}
}
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
noodle.input();
}
}
public class Soup {
public void input() { System.out.println("스프 넣기"); }
}
[복잡한 코드]
@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);
}
}
@Component
public class SimplePasswordEncoder {
public String 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);
}
}
String 이 아닌 StringBuilder 를 사용해야하는 이유
- 불변 Mutable : String
- 가변 Immutable : StringBuffer / StringBuilder
![]()
![]()
확장에 열려있고, 수정에 닫혀있다
개방 폐쇄 원칙을 지키기 위해서는 추상화에 의존 = 인터페이스에 구현체 교체
심화 : OCP 가 본질적으로 얘기하는 것은 추상화이며, 이는 결국 런타임 의존성과 컴파일 타임 의존성에 대한 것

[단순한 코드]
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
noodle.input();
}
}
public class Soup {
public void input() { System.out.println("스프 넣기"); }
}
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
noodle.input();
}
}
public interface Soup {
public abstract void input();
}
public class SinSoup implements Soup {
public void input() { System.out.println("신라면 스프 넣기"); }
}
public class JinSoup implements Soup {
public void input() { System.out.println("진라면 스프 넣기"); }
}
[복잡한 코드]
@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();
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final SHA256PasswordEncoder passwordEncoder;
...
}
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);
}
}
인터페이스 내에 메소드는 최소한 개수로 (S : 클래스, 메서드 관점 - I : 인터페이스 관점)
ex) 라면끓이기 객체
= 물 끓이기, 스프 넣기, 면 넣기, 파 썰어 넣기, 계란 넣기
- 파 썰어 넣기, 계란 넣기 내 아무 것도 구현이 없음 (텅 빈 Empty 구현)
- 기본 라면 끓이기, 라면에 고명넣기 등으로 쪼개어야함

[단순한 코드]
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
soup.onion();
soup.egg();
noodle.input();
}
}
public interface Soup {
public abstract void input();
public abstract void onion();
public abstract void egg();
}
public class SinSoup implements Soup {
public void input() { System.out.println("신라면 스프 넣기"); }
public void onion() { System.out.println("파 넣기"); }
public void egg() {}
}
public class JinSoup implements Soup {
public void input() { System.out.println("진라면 스프 넣기"); }
public void onion() {}
public void egg() { System.out.println("계란 넣기"); }
}
public interface Soup {
public abstract void input();
}
public interface Onion {
public abstract void input();
}
public interface Egg {
public abstract void input();
}
public class SinSoup implements Soup {
public void input() { System.out.println("신라면 스프 넣기"); }
}
public class GreenOnion implements Onion {
public void input() { System.out.println("파 넣기"); }
}
public class SmallEgg implements Egg {
public void input() { System.out.println("계란 넣기"); }
}
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Onion onion;
private final Egg egg;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
onion.input();
egg.input();
noodle.input();
}
}
[복잡한 코드]
@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);
}
}
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);
}
}
인터페이스로 구현체를 연결
고수준 모듈은 저수준 모듈의 구현에 의존하지 않는다는 건
= 저수준이 어떻게 구현되어있든 굳이 신경 쓸 필요가 없음
= 원하는 저수준 구현체를 가져다 쓰면 됨
= 저수준이 꼭 무엇이여야만 하는 것은 아님
= 저수준은 인터페이스를 통해 뚫려있고, 아무 구체 클래스만 들어오면 장땡

- 의존 역전 원칙(D)에서 의존성이 역전되는 시점은 “컴파일 시점” 이다.
← Dependancy Inversion (역전)- 개방 폐쇄 원칙(O)에서 구현체가 교체되는 시점은 “런타임 시점” 이다.
← Dependancy Injection (주입)

위 그림에서 PasswordEncoder 가 여러 구현체를 갖는 것(SimplePasswordEncoder 등)이 의존 역전 원칙(D)이 아니라, UserService 고수준 모듈이 PasswordEncoder 저수준 모듈에 의존하지 않고, PasswordEncoder 저수준 모듈이 UserService 고수준 모듈의 목적에 따라 선택되기때문에, 고수준 모듈에 의존한다고 할 수 있다.
의존 역전 원칙에서 의존성이 역전되는 시점은 컴파일 시점이라는 것이다. 런타임 시점에는 UserService가 SHA256PasswordEncoder라는 구체 클래스에 의존한다. 하지만 의존 역전 원칙은 컴파일 시점 또는 소스 코드 단계에서의 의존성이 역전되는 것을 의미하며, 코드에서는 UserService가 PasswordEncoder라는 인터페이스에 의존한다.

[단순한 코드]
public interface Soup {
public abstract void input();
}
public class SinSoup implements Soup {
public void input() { System.out.println("신라면 스프 넣기"); }
}
public class JinSoup implements Soup {
public void input() { System.out.println("진라면 스프 넣기"); }
}
@RequiredArgsConstructor
public class Ramen {
private final Water water;
private final Soup soup;
private final Onion onion;
private final Egg egg;
private final Noodle noodle;
public void make() {
water.input();
soup.input();
onion.input();
egg.input();
noodle.input();
}
}
public static void main(String[] args) {
Ramen ramen = new Ramen(
new Water(),
new JinSoup(),
new GreenOnion(),
new SmallEgg(),
new Noodle()
);
ramen.make();
}
“상속 시” 부모 클래스에 대한 가정 그대로 자식 클래스가 정의되어야한다
(계급제)
상속 시 하위 클래스는 상위 클래스의 모든 가정을 그대로 갖고있어야하며
상위 클래스를 사용하는 개발자가 가정하는 모든 것들 제공해야함
Rectangle rectangle = new Square();
// Rectangle = 상위 클래스 | Square = 하위 클래스
resize(rectangle, 100, 150);
// Rectangle 입장에선 말이되나, Square 입장에선 아예 말이 안되는 코드
// 개발자는 Rectangle 를 사용할 때 이게 정사각형인걸 가정할 수 없음

[복잡한 코드]
@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();
}
}
Rectangle rectangle = new Square();
resize(rectangle, 100, 150);
좋은 코드란 무엇인가? 변경이 용이하고, 유지보수와 확장이 쉬운 코드 (아까 배웠던 내용)
