development-logo
코드를 작성할 때 종종 “이 로직을 어떻게 단순하고 읽기 쉽게 표현할 수 있을까?” 고민하게 됩니다. 제가 수강 중인 “Readable Code: 읽기 좋은 코드를 작성하는 사고법” 강의 [논리, 사고의 흐름] 섹션에서는, 인지적 부담을 줄이고 가독성을 높이기 위해 고려해야 할 여러 포인트를 다루는데, 개인적으로 불필요한 else 제거 (Early Return), 중첩 분기/반복 최소화, 변수 선언 위치 등이 특히 인상 깊었습니다.
이 내용들을 학습하고 나니, 작은 코드 한 줄도 사람의 사고 방식을 기준으로 재배치하거나 단순화할 수 있다는 걸 체감했습니다. 오늘은 강의를 보고 개인적으로 느낀 점 그리고 함께 진행중인 인프런 워밍업 클럽 스터디 3기 미션까지 공유해 보겠습니다.
아래 코드는 사용자가 생성한 “주문(Order)”이 유효한지 검증하는 메서드입니다.
[섹션 3. 논리, 사고의 흐름]에서 이야기한 내용을 적용해 읽기 좋은 코드로 리팩토링해 봅시다.
원본 코드
public boolean validateOrder(Order order) {
if (order.getItems().size() == 0) {
log.info("주문 항목이 없습니다.");
return false;
} else {
if (order.getTotalPrice() > 0) {
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
} else {
return true;
}
} else if (!(order.getTotalPrice() > 0)) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
}
return true;
}
Javapublic boolean validateOrder(Order order) {
// 주문 항목 유무
if (order.getItems() == null || order.getItems().isEmpty()) {
log.info("주문 항목이 없습니다.");
return false;
}
// 총 가격 검증
if (order.getTotalPrice() <= 0) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
// 고객 정보 검증
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
// 여기까지 왔다면 유효한 주문
return true;
}
Javapublic boolean validateOrder(Order order) {
// Order에게 스스로 검사하도록 메시지(메서드)를 보낼 수 있음
// 내부 로직에서 "항목, 총가격, 사용자 정보"를 판단
return order.isValid();
}
Java// 예: Order 내부
public boolean isValid() {
if (this.items == null || this.items.isEmpty()) {
log.info("주문 항목이 없습니다.");
return false;
}
if (this.totalPrice <= 0) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (!this.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
Java어떠한 방법을 택하든, 중첩된 조건을 가능한 단순화하고, 각 조건의 의도를 빠르게 파악할 수 있도록 작성하는 것이 핵심이라고 생각합니다.
SOLID에 대하여 자기만의 언어로 정리해 봅시다.
미션에서 강조된 포인트 중 하나는, ‘자신의 언어로 표현할 수 있는가’ 였습니다.
아래는 제가 개인적으로 이해한 SOLID 각 원칙입니다.
정의
하나의 클래스(혹은 모듈)는 “오직 하나의 책임(변경 이유)”만 가져야 한다.
// SRP를 어긴 예시 (하나의 클래스가 여러 책임 수행)
public class OrderService {
// 주문 처리 로직
public void processOrder(Order order) {
// 비즈니스 로직...
}
// DB 접근 로직 (DAO 역할)
public void saveOrderToDatabase(Order order) {
// DB connection...
// SQL INSERT...
}
// 메일 발송 로직
public void sendOrderConfirmationMail(Order order) {
// SMTP 로직...
}
}
Java// SRP 준수 예시 (책임을 구분하여 클래스 분리)
public class OrderService {
private final OrderRepository orderRepository;
private final MailService mailService;
public OrderService(OrderRepository orderRepository, MailService mailService) {
this.orderRepository = orderRepository;
this.mailService = mailService;
}
// 주문 처리 (비즈니스 로직)
public void processOrder(Order order) {
// 1) 주문 관련 비즈니스 로직...
// 2) 주문 DB 저장은 OrderRepository에게 맡김
orderRepository.save(order);
// 3) 메일 발송은 MailService에게 맡김
mailService.sendConfirmation(order);
}
}
// DB 접근 전담
public class OrderRepository {
public void save(Order order) {
// DB 커넥션 및 SQL 로직...
}
}
// 메일 발송 전담
public class MailService {
public void sendConfirmation(Order order) {
// SMTP, 메일 전송 로직...
}
}
Java정의
확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
// OCP를 어긴 예시: 할인 정책 추가할 때마다 if문 수정
public class DiscountService {
// type으로 어떤 할인인지 구분
public int getDiscountPrice(int price, String discountType) {
if ("rate".equals(discountType)) {
return price - (int)(price * 0.1); // 10% 할인
} else if ("fixed".equals(discountType)) {
return price - 2000; // 2000원 할인
} else if ("vip".equals(discountType)) {
return price - 3000; // 추가로 VIP 할인
}
return price;
}
}
Java// OCP 준수 예시: 인터페이스로 추상화 -> 새로운 할인 정책을 추가할 때 확장
public interface DiscountPolicy {
int applyDiscount(int price);
}
public class RateDiscountPolicy implements DiscountPolicy {
@Override
public int applyDiscount(int price) {
return price - (int)(price * 0.1);
}
}
public class FixedDiscountPolicy implements DiscountPolicy {
@Override
public int applyDiscount(int price) {
return price - 2000;
}
}
// 사용처
public class DiscountService {
private final DiscountPolicy discountPolicy;
public DiscountService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public int getDiscountPrice(int price) {
return discountPolicy.applyDiscount(price);
}
}
Java정의
부모 클래스(인터페이스) 자리에 자식 클래스를 대입해도, 정상적으로 동작해야 한다.
// LSP를 어긴 예시
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() {
return width * height;
}
}
public class Square extends Rectangle {
// 정사각형은 width, height가 같아야 하므로
// 오버라이딩 시, LSP 위반 가능성
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 정사각형이므로 height도 width에 맞춤
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// LSP를 위반하는 케이스
// rectangle 객체로 치환했을 때 문제가 생김
public static void testRectangle(Rectangle r) {
r.setWidth(10);
r.setHeight(5);
// 우리가 기대하는 area는 10*5=50
// But Square로 치환되어 있으면 area()=25(정사각형이 돼버림)
}
Java해결
정의
클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다.
// ISP를 어긴 예시: 인터페이스가 너무 비대함
public interface MultiFunction {
void print(String text);
void scan(String filePath);
void fax(String number);
}
// 단순 프린터는 fax 기능 불필요
public class SimplePrinter implements MultiFunction {
@Override
public void print(String text) {
// print logic
}
@Override
public void scan(String filePath) {
// scan 필요없어도 구현 강제
}
@Override
public void fax(String number) {
// fax 필요없어도 구현 강제
}
}
Java// ISP 준수 예시: 인터페이스 분리
public interface Printer {
void print(String text);
}
public interface Scanner {
void scan(String filePath);
}
public interface Fax {
void fax(String number);
}
// 필요한 기능만 구현
public class SimplePrinter implements Printer {
@Override
public void print(String text) {
// print logic
}
}
Java정의
고수준 모듈(주요 로직)이 저수준 모듈(세부 구현)에 의존하지 않도록, 둘 다 추상화된 인터페이스에 의존하라.
// DIP 위반 예시: 고수준 모듈이 직접 저수준 구현을 생성하고 의존
public class OrderService {
private MySqlOrderRepository repository = new MySqlOrderRepository();
public void processOrder(Order order) {
repository.save(order);
}
}
Java// DIP 준수 예시: 추상화(인터페이스)에 의존
public class OrderService {
private final OrderRepository repository; // 인터페이스
public OrderService(OrderRepository repository) {
this.repository = repository; // 외부에서 구현체 주입
}
public void processOrder(Order order) {
repository.save(order);
}
}
public interface OrderRepository {
void save(Order order);
}
public class MySqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL 저장 로직...
}
}
public class MongoOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MongoDB 저장 로직...
}
}
Java이상 SOLID 각 원칙을 제 관점에서 풀어보고, 간단한 예시 코드를 곁들여 보았습니다.
이 5가지 원칙을 모두 지키면 객체지향 설계의 큰 틀이 잡히며, 유지보수성과 확장성이 좋아집니다.
논리, 사고의 흐름에서 강조된 내용들을 실습해보며, 중첩 if-else나 긴 메서드를 단순화하는 것만으로도 코드를 읽는 사람의 인지적 부담이 크게 줄어든다는 점을 체감했습니다.
또한 SOLID 원칙은 객체지향 설계를 할 때 왜 이렇게 나누고, 왜 이렇게 의존관계를 맺는가?에 대한 큰 방향을 잡아줍니다. 처음부터 완벽하게 지키기는 어렵지만, 코드를 리팩토링할 때마다 조금씩 적용해보면 확실히 확장성이나 가독성이 올라가는 걸 느낄 수 있습니다.
이상으로, 강의 내용을 바탕으로 읽기 좋은 코드에 대해 다시 생각해본 후기와 미션 과제 정리를 마칩니다.
감사합니다.
테스트 시 의존성 주입(Dependency Injection)과 Mockito Spring 애플리케이션을 개발하다 보면, 테스트 코드에서 실제 빈(Bean)을 사용하지…
들어가며 스프링 기반 프로젝트에서 좋은 설계 구조와 테스트 전략은 소프트웨어 품질과 유지보수성에 직결됩니다. 최근 학습한…
Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드 강의와 함께한 인프런 워밍업 클럽…
Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드 강의와 함께한 인프런 워밍업 클럽…
HTTP 상태코드란 무엇인가? HTTP 상태코드(HTTP Status Code)는 서버가 클라이언트의 요청을 처리한 결과를 수치화된 코드로 나타내는…