풀스택 개발자 과정 18일차

너구·2026년 6월 1일

풀스택 성장과정

목록 보기
18/79

메서드 체이닝 (Method Chaining)

메서드 체이닝은 메서드를 연속으로 이어서 호출하는 기법이다
자기 자신(this)를 반환해서 메서드를 연속으로 호출하는 기법이라고 볼 수 있다.

메서드 체이닝 예제 (1)

나쁜 예시

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    void greeting() {
        System.out.println("안녕 난" + name + "이라고 해");
    }

    void wakeUp() {
        System.out.println("좋은 아침!");
    }

    void haveBreakfast() {
        System.out.println("아침 먹는중");
    }

    void work() {
        System.out.println("일 하는중");
    }

    void sleep() {
        System.out.println("잠 자는중");
    }
}

public class Main {
    public static void main(String[] args) {
        Person john = new Person("John Doe");

        // 이 메서드들을 전부 호출해야 할 때 불편
        john.greeting();
        john.wakeUp();
        john.haveBreakfast();
        john.work();
        john.sleep();
    }
}

이 코드는 동작에는 문제가 없지만 객체지향 설계 관점에서는 사용하기 불편한 구조라는 점에서 나쁜 예시로 볼 수 있다.
현재는 john.greeting(); john.wakeUp();... 같은 코드들을 순서대로 호출해야 한다.
즉 사용하는 사람이 전체 과정을 전부 알고 있어야 한다.
또한 현재 구조는 올바른 순서를 보장하지 못한다는 문제가 있다.
만약 하루일과에 운동중이라는 일과가 추가되었다고 해보자.
그러면 Person만 수정하는 것이 아니라 사용하는 모든 코드도 수정해야 한다.
그렇다면 좋은 구조는 어떻게 될까?

좋은 예시

class Person{

    String name;

    Person(String name) {
        this.name = name;
    }

    Person greeting() {
        System.out.println("안녕 난" + name + "라고 해");
        return this; // this: 인스턴스 자신을 의미
    }

    Person wakeUp() {
        System.out.println("좋은 아침");
        return this;
    }

    Person haveBreakfast() {
        System.out.println("아침 먹는중");
        return this;
    }

    Person work() {
        System.out.println("일 하는중");
        return this;
    }

    Person sleep() {
        System.out.println("잠 자는중");
        return this;
    }
}

public class Main {
    public static void main(String[] args) {
        new Person("John Doe")
        .greeting()
        .wakeUp()
        .haveBreakfast()
        .work()
        .sleep();
    }
}

이전 코드에서는 메서드를 계속 하나씩 호출해야만 했다.
메서드가 많아질수록 코드가 길어지고 흐름을 한 눈에 보기 어려워진다.
하지만 지금은 메서드가 return this;를 통해 자기자신 (Person 객체)을 반환한다.
그래서 위와 같이 연속 호출이 가능해진다.
그럼 가독성이 좋아지고 코드가 간결해진다.
또한 각 메서드가 현재 객체(this)를 반환하므로 다음 메서드를 바로 호출할 수 있다.

메서드 체이닝 예제 (2)

나쁜 예시

class Chef {

    String bowl = "";

    void boilWater() {
        bowl += "물 ";
    }
    void addIngredients() {
        bowl += "건더기 ";
    }
    void addSpice() {
        bowl += "스프";
    }
    void cookNoodles() {
        bowl += "면 ";
    }
    // 완성된 라면을 서빙
    String serve() {
        return bowl;
    }
}

public class Main {
    public static void main(String[] args) {
        Chef chef = new Chef();

        chef.boilWater();
        chef.addIngredients();
        chef.addSpice();
        chef.cookNoodles();
        
        System.out.println(chef.serve());
    }
}

이번 예시 또한 위에 나쁜 예시와 동일한 문제점을 가지고 있다.
사용자가 순서를 직접 관리해야 하고 재료를 빼먹을 수도 있는 문제점이 생긴다.
Chef가 라면을 만드는 방법을 알고 있는데 정작 라면 조리 순서를 Main이 직접 관리하고 있다.
그렇다면 좋은 예시는 어떻게 작성할까?

좋은 예시

class Chef {

    // 가공/변환할 데이터
    String bowl = "";

    Chef boilWater() {
        bowl += "물 ";
        return this; // this: 인스턴스 자신을 의미
    }
    Chef addIngredients() {
        bowl += "건더기 ";
        return this;
    }
    Chef addSpice() {
        bowl += "스프";
        return this;
    }
    Chef cookNoodles() {
        bowl += "면 ";
        return this;
    }
    // 가공/변환한 데이터를 최종적으로 반환
    String serve() {
        return bowl;
    }
}

public class Main {
    public static void main(String[] args) {
        
        String ramen = new Chef()
        .boilWater()
        .addIngredients()
        .addSpice()
        .cookNoodles()
        .serve();

        System.out.println(ramen);
    }
}

메서드 체이닝의 핵심은 return this를 활용했다는 것이다.
this는 현재 Chef 객체 자신을 의미한다.
new Chef().boilWater()를 실행하면 다시 Chef 객체가 반환된다.
그래서 이어서 .boilWater .addSpice()처럼 계속 메서드를 호출할 수 있는 것이다.
객체 자신을 계속 반환해서 여러 작업을 자연스럽게 연결하는 좋은 메서드 체이닝 예제라고 볼 수 있다.


래퍼 클래스 (Wrapper Class)

큰 개념은 다른 클래스를 감싸는 클래스를 의미한다.
하지만 일반적으로 래퍼 클래스라고 하면 자바의 기본 타입을 감싸는 클래스를 의미한다.
자바는 객체지향 언이이지만 성능과 메모리 효율을 위해 숫자나 문자같은 단순한 값들을 객체가 아닌 '기본 타입'으로 처리한다.
개발을 하다보면 다음과 같이 반드시 객체만 들어가야 하는 상황이 있다.

  • 컬렉션 프라임워크 사용 (List, Set & Map 등)
  • 지네릭의 인자
  • null 처리

기본 자료형의 래퍼클래스

다음은 기본 타입과 그에 상응하는 래퍼클래스들이다.

래퍼클래스 객체 예제 (1)

int, bool, double, char 자료형의 값들을 래퍼클래스 타입으로 생성하라.

public class Main {
    public static void main(String[] args) {
        
        Integer n = 2;
        Boolean bool = true;
        Double d = 2.0;
        Character c = 'A';

        System.out.println(n);
        System.out.println(bool);
        System.out.println(d);
        System.out.println(c);
    }
}

이 코드를 보면 int, boolean, double, char 대신 다른 것을 사용하고 있다.
이들은 모두 자바의 래퍼클래스이다.
기본 자료형들은 값을 직접 저장하기 때문에 속도가 빠르고 메모리를 적게 사용한다.

래퍼 클래스는 기본 자료형을 객체로 감싼 클래스이다.
예를들어 Integer n = 2;는 내부적으로 숫자 2를 객체로 저장하는 것이다.
기본 자료형은 객체가 아니기 때문에 객체가 필요한 곳에서는 사용할 수 없다.

즉 래퍼 클래스는 기본 자료형을 객체처럼 사용할 수 있게 만들어주는 클래스라고 이해하면 된다.

래퍼클래스 객체 예제 (2)

정수형 배열리스트(ArrayList)를 생성하라.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {

        // 필요한 import 전부 가져오는 단축키: Shift + alt + o
        List<Integer> nums = new ArrayList<>();

        nums.add(10);
        nums.add(20);
        nums.add(30);

        System.out.println(nums);
    }
}

import란 코드 상단에 자바에서 제공하는 ArrayList와 List를 사용하기 위해 가져오는 코드이다.
마치 필요한 도구를 창고에서 꺼내오는 것과 비슷하다고 생각하면 된다.

List< Integer >란 숫자를 여러개 저장할 수 있는 리스트를 생성하는 코드이다.
< integer >는 리스트 안에 Integer 타입만 저장하겠다는 의미이다.
nums.add(10); nums.add(20);은 가능하지만 nums.add("hello");는 불가능하다.
문자열은 Integer가 아니기 때문이다.

리스트는 객체(Object)를 저장하는 자료구조이다.
그래서 int 대신 integer를 사용한다.
자바는 자동으로 nums.add(10);를 nums.add(Integer.valueOf(10)); 처럼 변환해준다.
이를 오토박싱(Auto Boxing)이라고 한다.

nums.add();로 리스트에 데이터를 추가해주고 System.out.println(nums);를 실행하면 [10, 20, 30]이 출력된다.
ArrayList는 내부적으로 toString() 메서드가 있어 저장된 데이터를 보기쉽게 출력해준다.


어노테이션 (Annotation)

부가적인 정보(메타 데이터)를 표현하기 위해 소스 코드에 추가하는 코드를 의미한다.
주해라고도 부른다.

@어노테이션

이런식으로 추가를 할 수 있다.

내장 어노테이션

다음은 자바에서 제공하는 주요 내장 어노테이션이다.

어노테이션 활용 예제

@Override 어노테이션을 활용해 오버라이딩을 검사하는 코드를 작성하라.

class Person {
    void greeting() {
        System.out.println("hello");
    }
}

class Korean extends Person {
    @Override
    void greeting() {
        System.out.println("안녕하세요");
    }
}

public class Main {
    
}

Override는 부모 클래스의 메서드를 자식 클래스가 재정의(덮어쓰기)하는 것이다.
@Oberride는 "나는 부모의 메서드를 오버라이드 하고 있습니다"라고 컴파일러에게 알려주는 표시이다.
만약 메서드 이름을 잘못 적으면 컴파일 오류가 발생해 실수를 바로 찾을 수 있다.


익명 클래스 (Anonymous Class)

일회성 객체를 만들 때 주로 사용한다.
주로 인터페이스나 추상 클래스의 추상 메서드를 빠르게 구현할 수 있다.

익명 클래스 구현하기 예제 (1)

다음 인터페이스를 구현하는 Korean, Japanese 클래스를 생성하라

<< interface >>
Person
greeing(): void

익명 클래스 X

interface Person {
    void greeting();
}

// 보통은 클래스들을 별도 파일로 생성
// 클래스가 많아지면 관리해야 하는 파일 수도 많아짐
class Korean implements Person {
    @Override
    public void greeting() {
        System.out.println("안녕하세요!");
    }
}

class Japanese implements Person {
    @Override
    public void greeting() {
        System.out.println("곤니치와!");
    }
}
public class Main {
    public static void main(String[] args) {
        
        Person korean = new Korean();
        korean.greeting();

        Person japanese = new Japanese();
        japanese.greeting();
    }
}

이 코드는 익명 클래스를 사용하지 않고 일반작인 구현 클래스를 만들어서 사용한 코드이다.
그렇다면 익명 클래스를 사용하면 어떻게 될까?

익명 클래스 사용 O

interface Person {
    void greeting();
}

// 관리해야할 클래스가 줄어들음
public class Main {
    public static void main(String[] args) {
        
        // korean, japanese 객체를 현재 파일에서만 사용하는 경우에 적합
        Person korean = new Person() {
            public void greeting() {
                System.out.println("안녕하세요!");
            }
        };

        korean.greeting();

        Person japanese = new Person() {
            public void greeting() {
                System.out.println("곤니치와!");
            };
        };

        japanese.greeting();
    }
}

보통은 첫번째처럼 클래스를 따로 만들지만 이번 코드는 new Person() {...} 안에서 인터페이스를 바로 구현하고 있다.
Korean 클래스도 Japanese 클래스도 없고 객체를 만드는 순간 구현까지 같이한 것인데 그래서 이름 없는 클래스 = 익명 클래스라고 부른다.

장점은 클래스의 개수가 줄어든다.
일반적인 방식은 class ~ class ~처럼 클래스가 계속 늘어나지만 익명 클래스는 별도의 클래스 파일이 필요하지가 않다.
예를 들어 korean.greeting(); 한 번만 쓰고 끝날 객체라면 굳이 class Korean을 만들 필요가 없다.
또한 main 안에서 바로 구현하기 때문에 코드를 읽을 때 이동할 필요도 적어진다.
그래서 한 번만 사용할 객체는 익명 클래스, 여러 곳에서 사용할 객체는 일반 클래스로 구분해서 사용하는 경우가 많다.

익명 클래스 구현하기 예제 (2)

익명 클래스를 활용해 다음 Pet 인터페이스를 구현한 Dog, Cat 객체를 생성하라.

<< interface >>
Pet
cry(): void

interface Pet {
    void cry();
}

public class Main {
    public static void main(String[] args) {
        Pet dog = new Pet() {
            public void cry() {
                System.out.println("멍멍");
            }
        };

        dog.cry();

        Pet cat = new Pet() {
            public void cry() {
                System.out.println("야옹");
            }
        };

        cat.cry();
    }
}

별도의 Dog, Cat 클래스를 만들지 않고 new Pet() {...} 형태로 Pet 인터페이스를 즉시 구현하였다.
따라서 클래스 수를 줄일 수 있었고 한 번만 사용할 객체를 간단하게 만들 수 있다라는 장점이 있다는 것을 확인하였다.


상수 (Constants)

변하지 않는 값을 의미한다.
자바에서는 따로 상수 키워드가 존재하지 않고 static fianl로 상수를 만든다.
상수 이름은 대문자와 언더스코어를 조합해 이름을 짓는 것이 관습니다.
ex)MAYH_PI

상수 선언 예제

앱 설정(App Configuration)에 아래의 상수를 선언하라

  • DB_URL (데이터베이스 주소)
  • API_URL (API 주소)
  • MAX_LOGIN_ATTEMPTS (최대 로그인 횟수)
class AppConfig { // 설정(Configuration)
    // 데이터베이스 주소
    static final String DB_URL = "localhost:3306/mydb";
    // API 주소
    static final String API_URL = "https://exaple.com/api";
    // 최대 로그인 횟수
    static final int MAX_LOGIN_ATTEMPTS = 5;
}

public class Main {
    public static void main(String[] args) {
        // static 변수는 클래스 자체로 접근한다
        System.out.println(AppConfig.DB_URL);
        System.out.println(AppConfig.API_URL);
        System.out.println(AppConfig.MAX_LOGIN_ATTEMPTS);
    }
}

이 코드는 프로그램에서 공통으로 사용하는 설정값을 한 곳에 모아 관리하는 예제이다.
static final을 사용했기 때문에 static 개체를 생성하지 않아도 클래스명으로 바로 접근할 수 있다.
final은 값을 변경할 수 없는 상수를 뜻한다.
그래서 Appconfig.DB_URL 처럼 객체를 만들지 않고 사용할 수 있으며 DB_URL, API_URL, MAX_LOGIN_ATTEMPTS 값이 프로그램 실행 중에 변경되지 않는다.
즉 자주 사용하는 설정값을 한 곳에서 관리하여 유지보수를 쉽게하는 구조라고 볼 수 있다.

핵심은 이거다.
이 코드에서는 DB_URL, API_URL, MAX_LOGIN_ATTEMPTS를 static final로 선언하여 프로그램 전체에서 공통으로 사용하는 변경 불가능한 설정값(상수)로 만들었다는 것이다.
덕분에 코드의 가독성이 좋아지고, 설정값 변경 시 유지보수가 쉬워진다는 장점이 있다.


enum

열거형(Enumeration)이라고 부르며 관련된 상수들의 집합이다.
C언어나 C++의 Enum은 단순한 정수(Integer)에 이름을 붙인 것에 불과하지만 자바의 Enum은 완전한 하나의 클래스이다.
따라서 내부에 필드(변수), 생성자, 메서드를 가질 수 있다.

enum 생성 예제 (1)

요일을 나타내는 enum을 아래를 참고하여 생성하라.

<< enum >>
Day
SUN
MON
TUE
WED
THU
FRI
SAT

enum Day {
    SUN,
    MON,
    TUE,
    WED,
    THU,
    FRI,
    SAT
};

public class Main {
  public static void main(String[] args) {
    
    // 각 상수에 접근하기
    System.out.println(Day.SUN);
    System.out.println(Day.MON);
    System.out.println(Day.TUE);
    System.out.println(Day.WED);
    System.out.println(Day.THU);
    System.out.println(Day.FRI);
    System.out.println(Day.SAT);
  }  
}

Enum은 관련된 상수들을 하나의 타입으로 묶어서 관리하는 기능이다.
예를들어 요일은 SUN, MON, TUE... 처럼 정해진 값만 사용해야 한다.
이럴 때 Enum을 사용하면 요일을 안전하게 관리할 수 있다.

여기서 Day는 Enum 타입이다.
SUN, MON, TUE...들은 Day 타입에 속한 상수들이다.
즉 Day.SUN 은 일요일, Day.MON은 월요일을 의미한다.

Enum의 상수는 클래스처럼 접근한다.
System.out.println(Day.SUN); 를 출력하면 SUN이 나온다.

이렇게 Enum을 사용하면 Day day = Day.MON;처럼 정해진 값만 사용할 수 있다.
그래서 오타를 방지하고 코드의 안정성을 높일 수 있다.

즉, 요일처럼 정해진 값들만 사용해야 할 때 Enum을 사용하면 더 안전하고 가독성 좋은 코드를 만들 수 있다.

eunm 생성 예제 (2)

주문 상태를 나타내는 enum을 아래 그림을 참고해 생성하라.

<< enum >>
OrderStatus
READY(배송준비)
SHIPPING(배송중)
DELIVERED(배송완료)

enum OrderStatus {
    READY,
    SHIPPING,
    DELIVERED
};

public class Main {
    public static void main(String[] args) {
        System.out.println(OrderStatus.READY);
        System.out.println(OrderStatus.SHIPPING);
        System.out.println(OrderStatus.DELIVERED);
    }  
}

eunm을 통해 주문상태를 관리하는 코드를 작성하였다.
주문상태는 정해진 값만 사용해야 한다.
String status = "배송중"; 처럼 문자열을 사용하면 오타가 발생할 수 있지만 OrderStatus starus = OrderStatus.SHIPPING;처럼 enum을 사용하면 정해진 상태만 사용할 수 있어 안전하다.

enum 생성 예제 (3)

switch 문을 활용해 다음과 같이 출력하라.

enum Day { SUN, MON, TUE, WED, THU, FRI, SAT }

public class Main {
    public static void main(String[] args) {
        
        Day today = Day.FRI;

        switch (today) {
            case Day.SUN:
                System.out.println("일요일입니다");
                break;
            case Day.MON:
                System.out.println("월요일입니다");
                break;
            case Day.TUE:
                System.out.println("화요일입니다");
                break;
            case Day.WED:
                System.out.println("수요일입니다");
                break;
            case Day.THU:
                System.out.println("목요일입니다");
                break;
            case Day.FRI:
                System.out.println("금요일입니다");
                break;
            case Day.SAT:
                System.out.println("토요일입니다");
                break;
        }
    }
}

이 코드는 enum과 switch문을 함께 사용해 요일에 따라 다른 동작을 수행한다.
먼저 Day라는 enum을 만들었다.
이 enum은 요일을 의미하는 상수들을 하나의 타입으로 묶어 관리한다.
그리고 Day today = Day.FRI; 를 통해 오늘을 금요일로 설정했다.
이후 switch문을 사용해 today의 값에 따라 다른 코드를 실행한다.
현재 today가 Day.FRI이므로 case Day.FRI; 부분이 실행되어 "금요일입니다"가 출력된다.

enum 생성 예제 (4)

앞에서 만든 OrderStatus를 활용해 다음을 만족하는 switch문을 생성하라

public class Main {
    public static void main(String[] args) {
        
        OrderStatus now = OrderStatus.READY;

        switch(now) {
            case OrderStatus.READY:
                System.out.println("배송준비");
                break;
            
            case OrderStatus.SHIPPING:
                System.out.println("배송중");
                break;

            case OrderStatus.DELIVERED:
                System.out.println("배송완료");
                break;
        }
    }
}

현재 주문상태를 READY로 설정한다.
그리고 switch문을 활용해 현재 상태가 무엇인지 확인한다.
현재(now)가 OrderStatus.READY 이므로 case OrderStatus.READY가 실행되어 "배송준비"가 출력된다.

실행에는 문제없이 작성했지만 enum을 switch에서 사용할 때는 case에 enum타입 이름을 붙이지 않는 것이 정석이라고 한다.

case OrderStatus.READY라고 작성하였지만 case READY라고만 작성해도 된다라는 것이다.
switch(now)에서 now의 타입이 OrderStatus라는 것을 알고있기 때문이다.
그래서 Java는 case READY만 봐도 case OrderStatus.READY를 의미한다고 판단한다.


지네릭 (Generics)

지네릭은 타입 매개변수이다.
지네릭을 활용해 자바 프로그래밍을 더 유연하게 할 수있다.
지네릭 클래스와 지네릭 매개변수가 있다.

  • Generic Class
  • Generic Methods

지네릭 클래스

class 클래스이름 < T >

지네릭 클래스를 정의할 땐 위처럼 정의합니다.
T는 타입 매개변수를 의미하며 객체 생성 과정에서 전달합니다.

지네릭 클래스 생성 예제 (1)

다음을 만족하는 지네릭 클래스를 생성하고 객체를 생성하라.

Box< T >
content: T
getContent(): T

class Box<T> { // T: 타입 매개변수

    T content;

    Box(T content) {
        this.content = content;
    }

    T getContent() {
        return content;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("hello");
        System.out.println("내용물: " + stringBox.getContent());

        Box<Integer> numberBox = new Box<>(999);
        System.out.println("내용물: " + numberBox.getContent());

        Box<Boolean> boolBox = new Box<>(true);
        System.out.println("내용물: " + boolBox.getContent());
    }
}

지네릭 클래스는 데이터 타입을 미리 정하지 않고 나중에 결정할 수 있게 만든 클래스이다.
이 코드에서 T가 바로 타입 매개변수(Type Parameter)이다.
즉 T 자리에 나중에 원하는 타입을 넣어서 사용할 수 있다.

Box< String > stringBox = new Box<>("hello");
여기서는 T가 String이 된다.
즉 실제로는 String content;처럼 동작한다.

Box< Integer > numberBox = new Box<>(999);
여기서는 T가 Integer가 된다.

Box< Boolean > boolBox = new Box<>(true);
여기서는 T가 Boolean이 된다.

만약 지네릭이 없다면 class StringBox{}, class IntergerBox{}...처럼 타입마다 클래스를 따로 만들어야 한다.
하지만 지네릭을 사용하면 Box< T > 하나로 모든 타입을 처리할 수 있다.


오늘은 메서드 체이닝(Method Chaining), 래퍼 클래스(Wrapper Class), 어노테이션(Annotation), 익명 클래스(Anonymous Class), 상수(Constants), Enum, 그리고 지네릭(Generics)까지 자바에서 자주 사용되는 여러 기능들을 학습해보았다.

메서드 체이닝을 통해 객체 자신을 반환하는 return this의 의미를 이해할 수 있었고, 코드를 더 자연스럽고 읽기 쉽게 작성하는 방법을 배웠다. 또한 래퍼 클래스를 통해 기본 자료형과 객체의 차이를 이해할 수 있었고, 컬렉션과 지네릭에서 왜 Integer, Boolean 같은 타입을 사용하는지도 알게 되었다.

어노테이션에서는 @Override를 활용하여 오버라이딩을 더욱 안전하게 작성하는 방법을 배웠고, 익명 클래스를 통해 한 번만 사용할 객체를 간단하게 생성하는 방법도 익힐 수 있었다. 상수와 Enum은 프로그램에서 변하지 않는 값이나 정해진 상태를 더 안전하게 관리하기 위한 방법이라는 점을 확인할 수 있었으며, 지네릭은 하나의 클래스로 여러 타입을 처리할 수 있게 해주는 강력한 기능이라는 것을 이해할 수 있었다.

특히 오늘 배운 내용들은 단순히 문법을 외우는 것이 아니라 "왜 이런 기능이 필요한가?"를 생각해보는 시간이었다. 처음에는 낯설게 느껴질 수 있지만, 실제로 코드를 작성하면서 사용해보니 유지보수성과 재사용성을 높이기 위해 만들어진 기능들이라는 점을 조금씩 체감할 수 있었다.

앞으로도 단순히 동작하는 코드를 만드는 것을 넘어, 더 읽기 쉽고 확장하기 쉬운 코드를 작성할 수 있도록 객체지향적인 사고방식을 계속 연습해 나가야겠다. 작은 개념 하나하나를 확실하게 이해하며 꾸준히 쌓아가는 것이 결국 좋은 개발자로 성장하는 가장 빠른 길이라는 생각이 든다.

0개의 댓글