Skip to content
woo jin edited this page Dec 22, 2025 · 14 revisions

📚 추상(抽象)

  • 가독성(Readability) = 글이 잘 읽힌다 = 이해가 잘 된다 = 유지보수 하기 수월하다 = 시간과 자원이 절약된다.

추상(abstract) vs 구체(concrete)

  • 추상 = 특정 측면을 가려내고 불필요한 부분을 버린다.

네이밍(naming)🤔

  • ✅️이름을 짓는다는 행위는 추상적 사고를 기반으로 한다.
    • 표현하고자 하는 구체에서 정말 중요한 핵심 개념만을 추출해 잘 드러내는 표현
    • 우리 도메인의 문맥 안에서 이해되는 용어
  • ✅️단수와 복수를 구분하자.
    • 끝에 -(e)s를 붙여 어떤 데이터가 단수인지 복수인지를 나타내는 것만으로도 읽는 이에게 중요한 정보를 같이 전달할 수 있다.
  • ✅️이름을 줄이지 않는다.
    • 가독성을 희생해 얻는 부분으로 잃는 것에 비해 얻는 것이 적다.
  • ✅️은어/방언 사용하지 않는다.
    • 도메인 용어를 먼저 정의하는 과정이 먼저 필요할 수도 있다. 모두가 아는 것으로 사용하는 것이 좋다.

메서드 추상화(method abstraction)🤔

  • ✅️한 메서드의 주제는 반드시 하나다.
    • 메서드의 이름으로 구체적인 내용을 추상화한다.
  • ✅️생략할 정보와 의미를 부여하고 드러낼 정보를 구분한다.

메서드 선언부🤔

  • ✅️void 대신 반환할 만한 값이 있는지 고민해본다. -> 반환값이 있다면 테스트가 용이하기 때문에

매직 넘버, 매직 스트링🤔

  • ✅️매직 넘버/매직 스트링 : 의미를 갖고 있으나 상수로 추출되지 않은 숫자, 문자열 등
    • 이런 상수 추출을 통해 이름을 짓고 의미를 부여함으로써 기독성과 유지보수성을 높인다.
스크린샷 2025-09-10 11 01 07

📚 논리/사고의 흐름

Early Return

  • Early Return으로 else의 사용을 지양하자.
// bad❌
int get_string_type(char* string) {
    int ret = 0;
    if (is_rule_1(string)) {
        ret = 1;
    } else {
        if (is_rule_2(string)) {
            ret = 2;
        } else {
            ret = 0;
        }
    }

    return ret;
}

// good⭕
int is_valid(char* string) {
    if (is_rule_1(string))
        return 1;
    if (is_rule_2(string))
        return 2;

    return 0;
}

공백 라인의 의미

  • 복잡한 로직의 끊어 읽기가 필요한 경우

부정어를 대하는 자세

// bad❌ -> 2번의 사고를 필요로 한다.
if (!isLeftDirection()) {
   doSomething();
}

// good⭕ -> 1번이면 끝
if (isNotLeftDirection()) {
  doSomething();
}
  • 부정어구를 쓰지 않아도 되는 상황인가를 체크한다.
  • 부정어 의미를 담고 있는 다른 단어가 존재하는지 고민하고 직접 부정어구로 메서드명을 구성한다.
    • 부정 연산자(!)의 가독성 저해

해피 케이스와 예외 처리

  • 예외가 발생할 가능성을 낮추는 것에 초점을 맞춰야 한다.
  • 의도한 예외와 의도치않은 예외를 구분한다.
    • 사용자에게 보여줄 예외와 개발자가 보고 처리해야 할 예외를 구분한다.

Null

  • NPE를 방지하는 방향으로 경각심을 가져야 한다.
  • 메서드 설계 시 return null을 자제한다. 만약 어렵다면 Optional 사용을 고려해본다.
  • Optional
    • Optional은 비싼 객체이다. 꼭 필요한 상황에서 반환 타입에 사용한다.
    • Optional은 절대로 파라미터로 받지 않도록 한다. 분기 케이스를 따져야하기 때문이다.
    • Optional을 반환받았다면 빠르게 해소한다.
    • orElse(), orElseGet(), orElseThrow() 차이를 숙지하고 사용하도록 한다.

1. orElseThrow() : 값이 없는 것을 '예외'로 처리할 때

  • Optional이 비어있을 경우, 지정된 예외(Exception)를 발생시킨다.

2. orElseGet() : 값이 없을 때 '대체 값을 동적으로 생성'해야 할 때

  • Optional이 비어있을 경우에만, 파라미터로 받은 Supplier (람다식 또는 메서드 레퍼런스)를 실행하여 그 결과값을 반환한다.
    • 대체할 기본값이 새로운 객체 생성을 필요로 할 때
    • 대체할 값을 가져오기 위해 다른 메서드를 호출하거나 복잡한 연산이 필요할 때

3. orElse() : 값이 없을 때 '이미 만들어진 상수 값'을 사용할 때

  • Optional이 비어있을 경우, 파라미터로 받은 값을 그대로 반환한다.
  • 불필요한 연산 : orElse() 안에 객체 생성이나 메서드 호출 코드를 넣으면, Optional에 값이 존재해서 그 결과가 필요 없음에도 불구하고 무조건 실행되어 성능 저하의 원인이 될 수 있다.

📚 객체 지향 패러다임

  • 객체(Object) = 데이터 + 코드
  • 객체 추상화 필요 단계
    • 비공개 필드(데이터) + 비공개 로직(코드)
    • 공개 메서드 선언부를 통해 외부 세계와 소통
    • 객체 책임이 나뉨에 따라 객체 간 협력이 발생

객체를 만들 때 주의할 점

  • ✅️1개의 관심사로 명확하게 책임이 정의되었는가?
  • ✅️생성자 혹은 정적 팩토리 메서드에서 유효성 검증이 가능하다.
    • 도메인에 특화된 검증 로직이 들어갈 수 있다.
class Money {
    private long value;

    public Money(long value) {
        if (value < 0) {
            throw new IllegalArgumentException("돈은 0원 이상이어야 합니다.");
        }
        this.value = value;
    }
}
  • ✅️setter 사용은 자제한다.
    • 데이터는 불변이 최고다. 변하는 데이터라도 객체가 핸들링할 수 있어야 한다.
    • 객체 내부에서 외부 세계의 개입 없이 자체적인 변경/가공으로 처리할 수 있는가를 확인한다.
    • 만약 외부에서 가지고 있는 데이터로 데이터 변경 요청을 하는 경우 set~보다는 update~와 같이 의도를 드러내는 네이밍을 고려한다.
  • ✅️getter도 처음엔 사용을 자제한다. 필요한 경우에만 추가하도록 한다.
    • 외부에서 객체 내의 데이터가 필요하다고 getter를 남발하는 것은 무례한 행동이다. 객체에 메시지를 요청한다.
Person person = new Person();

// bad❌
if (person.getWallet().getID().findAge() >= 19) {
  pass();
}

// good⭕
if (person.isAgeGraterThanOrEqualTo(19) {
  pass();
}
  • ✅️객체의 필드 수를 줄여라.
    • 불필요한 데이터가 많을수록 복잡도가 높아지고 대응할 변화가 많아진다.
    • 메서드 기능으로도 제공이 가능하면 OK, 단 미리 가공하는 것이 성능 상의 이점이 있다면 필드로 가지고 있는 것도 좋을 순 있다.
class Bill {
    private final List<Menu> menus;
    private final long totalPrice;  // 미리 가지고 있을수도 있는 값

    // 메서드 기능으로 충분히 계산할 수 있는 값
    public long calculateTotalPrice() {
        return this.menus.stream()
                   .mapToLong(Menu::getPrice)
                   .sum();
    }
}

SOLID - SRP(단일 책임 원칙)

  • ✅️하나의 클래스는 단 한 가지 변경 이유만을 가져야 한다.
    • 객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경이 되어야 한다.

SOLID - OCP(개방 폐쇄 원칙)

  • ✅️확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
    • 기존 코드 변경없이, 시스템 기능을 확장할 수 있어야 한다.
    • 추상화와 다형성을 활용해 OCP를 지킬 수 있다. 구체에 의존하기보다 인터페이스로 분리해야 한다.
  • 무엇이, 왜 변할 것인가?
  • 변하는 것을 '약속(인터페이스)'으로 만들었는가?
  • '역할'과 '책임'을 명확히 분리했는가?
  • '상속' 대신 '구성'을 사용하고 있는가?

SOLID - LSP(리스코프 치환 원칙)

  • ✅️상속 구조에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.
    • 자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.
    • LSP를 위반하면 상속 클래스를 사용할 때 오동작, 예상 박의 예외가 발생하거나 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.

SOLID - ISP(인터페이스 분리 원칙)

  • ✅️클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다. 그러니 인터페이스를 잘게 쪼개라.
    • ISP를 위반하면 불필요한 의존성으로 인해 결합도가 높아지고 특정 기능 변경이 여러 클래스에 영향을 미칠 수 있다.

SOLID - DIP(의존 관계 주입 원칙)

  • ✅️상위 수준 모듈은 하위 수준 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.

📚 객체 지향 적용하기

스크린샷 2025-09-07 오후 2 11 47 스크린샷 2025-09-07 오후 2 12 11
  • 상속보다는 조합을 사용하자.
  • 상속은 시멘트처럼 굳어지는 구조다. 수정이 어렵다.
  • 조합과 인터페이스를 활용하는 것이 유연한 구조를 만든다.

Value Object(VO)

  • 기본 타입을 객체로 감싸고 의미를 부여해 추상화
  • 도메인의 어떤 개념을 추상화하여 표현한 값 객체
  • 값으로 취급하기 위해서 불변성, 동등성, 유효성 검증 등을 보장해야 한다.
    • 불변성 : final 필드, setter 금지
    • 동등성 : 서로 다른 인스턴스여도 내부 값이 같으면 같은 값 객체로 취급한다. 이 때, equals()hashCode() 재정의가 필요하다.
    • 유효성 검증 : 객체가 생성되는 시점에 대해 유효성을 검증
public class Money {
    
    private final long value;

    public Money(long value) {
        if (value < 0) {
            throw new IllegalArgumentException("돈은 0원 이상이어야 합니다.");
        }
        this.value = value;
    }

    // equals() & hashCode() 재정의
}

Money money1 = new Money(10000);
Money money2 = new Money(10000); // 같은 10000원이라는 표면적 가치를 지니기 때문에 같은 돈이다.
  • Entity는 식별자가 존재한다. 식별자가 아닌 필드의 값이 달라도 식별자가 같으면 동등한 객체로 취급한다.
    • equals() & hashCode()도 식별자 필드만 가지고 재정의할 수 있다.
    • 식별자가 같은데 식별자가 아닌 필드 값이 서로 다른 두 인스턴스가 있다면 시간이 지남에 따라 변화한 것으로 볼 수 있다.
  • VO는 식별자 없이 내부 모든 값이 다 같아야 동등한 객체로 취급한다.

참고 - VO vs Entity

// bad❌
public class Lotto {
    private final List<Integer> numbers; // 로또 번호를 List로 직접 관리

    public Lotto(List<Integer> numbers) {
        // 유효성 검증 로직이 외부에 노출되거나 중복될 가능성이 높음
        if (numbers.size() != 6) {
            throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
        }
        this.numbers = numbers;
    }

    // numbers를 직접 반환하여 외부에서 수정될 위험이 있음 (getNumbers().add(7))
    public List<Integer> getNumbers() {
        return numbers;
    }

    public boolean contains(int number) {
        // 컬렉션에 대한 단순 위임 메서드
        return numbers.contains(number);
    }
}

// good⭕
public class LottoNumbers {
    private static final int LOTTO_NUMBER_SIZE = 6;

    private final List<Integer> numbers;

    public LottoNumbers(List<Integer> numbers) {
        validateSize(numbers); // 생성 시점에 유효성 검증
        validateDuplicate(numbers); // 중복 검증
        this.numbers = numbers;
    }

    // 비즈니스 로직을 내부에 포함 (예: 특정 숫자를 포함하는지)
    public boolean contains(int number) {
        return this.numbers.contains(number);
    }

    // 당첨 번호와 몇 개가 일치하는지 계산하는 비즈니스 로직
    public int countMatch(LottoNumbers winningNumbers) {
        return (int) this.numbers.stream()
                .filter(winningNumbers::contains)
                .count();
    }
    
    // 외부에서 컬렉션을 직접 수정하지 못하도록 방어적 복사 또는 unmodifiableList 반환
    public List<Integer> getNumbers() {
        return Collections.unmodifiableList(numbers);
    }

    private void validateSize(List<Integer> numbers) {
        if (numbers.size() != LOTTO_NUMBER_SIZE) {
            throw new IllegalArgumentException("로또 번호는 " + LOTTO_NUMBER_SIZE + "개여야 합니다.");
        }
    }

    private void validateDuplicate(List<Integer> numbers) {
        if (numbers.stream().distinct().count() != numbers.size()) {
            throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다.");
        }
    }
}

// Lotto 클래스는 이제 LottoNumbers 일급 컬렉션을 사용
public class Lotto {
    private final LottoNumbers lottoNumbers;
    private final int bonusNumber;

    public Lotto(LottoNumbers lottoNumbers, int bonusNumber) {
        this.lottoNumbers = lottoNumbers;
        this.bonusNumber = bonusNumber;
    }
    
    // ... 당첨 여부 확인 등 Lotto와 관련된 다른 로직 ...
}

Enum

  • Enum은 상수의 집합이며 상수와 관련된 로직을 담을 수 있는 공간이다.
  • 특정 도메인 개념에 대해 그 종류와 기능을 명시적으로 표현해줄 수 있다.
  • 변경이 정말 잦은 개념은 Enum보다 DB로 관리하는 것이 나을 수 있다.

📚 코드

주석(comment)

사실 주석에 대한 부분은 여태까지 개발을 하면서 크게 신경을 쓰지 않았다. 오히려 나는 주석이라는 것이 이러이러한 것임을 설명하기에 좋다고 생각했었다.

  • 주석을 작성할 때 자주 변하는 정보는 최대한 지양해서 작성한다.
  • 만약 관련 정책이 변하거나 코드가 변경되었다면 주석도 잊지 않고 함께 업데이트한다.
  • 좋은 주석이란, 모든 표현 방법을 총동원해 코드에 의도를 녹여내고 그럼에도 불구하고 전달해야 할 정보가 남았을 때 사용하는 것이 좋은 주석이 된다.

변수와 메서드의 나열 순서

개발하면서 파라미터의 순서나 변수, 메서드 등의 순서도 크게 신경쓰지 않았던 것 같다.

  • 변수는 사용하는 순서대로 나열한다.
  • 메서드의 순서도 고려해보아야 하는데, 객체 입장에서 생각해본다.

오버 엔지니어링

  • 필요한 적정 수준보다 더 높은 수준의 엔지니어링
  • 구현체가 하나인 인터페이스
    • 코드 탐색에 영향을 줄 수 있다.
  • 너무 이른 추상화
    • 정보가 숨겨지기 때문에 오히려 복잡도가 높아진다.
    • 후대 개발자들이 선대의 의도를 파악하기 어렵다.

은탄환은 없다

  • 지속 가능한 소프트웨어의 품질이냐 기술 부채를 안고 가는 빠른 결과물이냐
  • 클린 코드를 추구하지 말라는 것이 아니라 미래 시점에서도 잘 고칠 수 있도록 하는 코드 센스가 필요

패키지 나누기

  • 패키지는 문맥으로써의 정보를 제공할 수 있는 중요한 요소다.
  • 패키지를 쪼개지 않으면 관리가 어려워진다.
  • 그렇다고 해서 패키지를 너무 잘게 쪼개면 마찬가지로 관리가 어려워진다.
  • ✅️대규모 패키지 변경은 팀원과의 합의를 이룬 시점에 하자.
    • 현재 기준으로 본인만 변경하고 있는 부분이라면 괜찮으나 여러 사람이 변경 중인 부분이나 공통으로 사용하는 클래스 패키지를 한 번에 변경하면 추후 Conflict가 발생할 수 있다.
    • 처음 만들 때부터 잘 고민해서 패키지를 나눠놓는 것이 제일 좋다.

Entity vs VO vs DTO(사수로부터의 피드백)

DTO

  • DTO(Data Transfer Object)는 데이터를 전달하기 위한 객체이다. 계층 간 데이터를 주고받을 때, 데이터를 담아서 전달한다.
  • 여러 레이어 사이에서 DTO를 사용할 수 있다.
  • DTO는 Getter/Setter 메서드를 포함한다. 이외의 비즈니스 로직은 포함하지 않는다.

VO

  • VO는 값 자체를 표현하는 객체이다.
  • VO는 객체들의 주소가 달라도 값이 같으면 동일한 것으로 간주한다.
  • 고유번호가 서로 다른 10,000원 지폐 2장이 있다고 가정할 때, 이 두 지폐의 고유번호는 다르지만 액면가가 같기 때문에 동일하다고 말할 수 있다.
  • VO는 Getter 메서드와 함께 비즈니스 로직도 포함할 수 있다.
  • 단, Setter 메서드는 가지지 않는다.(불변성을 보장해야하기 때문에)
  • 또, 값 비교를 위해 equals(), hashCode() 메서드를 오버라이딩 해줘야 한다.
  • Java의 record 타입은 자바 16에 도입된 특별한 유형의 클래스로, 불변 데이터를 간편하게 저장하기 위해 설계되었다.
  • 컴파일러가 메서드를 구성 요소 기반으로 자동으로 구현해주기 때문에 직접 구현할 필요가 없이 바로 사용 가능하다.

Entity

  • Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이다.
  • 이를 기준으로 테이블이 생성되고 스키마가 변경된다.
  • 따라서, 절대로 Entity를 요청이나 응답값을 전달하는 클래스로 사용해선 안 된다.
  • Entity는 기본적으로 PK(Primary Key) 즉, ID로 식별이 되고 비즈니스 로직을 포함할 수 있다.

📚 IDE의 도움 받기

  • 코드 포맷 정렬 : Option + Cmd + L
  • SonarQube : 잠재적인 문제가 될 수 있는 오류, 버그, 스타일 등을 미리 알려주는 코드 품질 체크 도구

📖 Java

📖 Kotlin

📖 Coroutine

📖 Spring

📖 Spring Security

📖 Spring Batch

📖 Reactive Programming

📖 Database

📖 MySQL

📖 Redis

📖 JPA

📖 QueryDsl

📖 MSA

📖 Kafka

📖 Apache Flink

  • [Apache Flink - Apache Flink Architecture]
  • [Apache Flink - Stream Processing]
  • [Apache Flink - Data Stream API & Window]
  • [Apache Flink - State Management]

📖 HTTP

📖 AWS

📖 Docker

📖 Kubernetes

📖 CI/CD

📖 Nginx

📖 Monitoring🥈

  • [Monitoring - Log Concept]
  • [Monitoring - Log Level & Filter]
  • [Monitoring - Logback]
  • [Monitoring - Log Collection with ELK Stack]
  • [Monitoring - Log Monitoring with Kibana]
  • [Monitoring - Building a Monitoring System with Spring Boot Actuator]
  • [Monitoring - Server Monitoring with Prometheus and Grafana with Discord Alerts]

📖 Test

📖 Effective Java 3/E

📖 Kotlin Academy - Effective Kotlin

📖 Kotlin Academy - 핵심편

📖 스프링으로 시작하는 리액티브 프로그래밍

📖 가상 면접 사례로 배우는 대규모 시스템 설계 기초 1

📖 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2

📖 Clean Code

📖 리팩토링 2판

📖 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식

📖 GraphQL

Clone this wiki locally