개요

오늘은 객체 지향 프로그래밍(OOP)의 핵심 개념인 상속(Inheritance)과 복합(Composition)에 대해 설명해드리겠습니다. 이 두 개념은 코드 재사용과 유지보수성을 높이는 중요한 설계 방식입니다. 예제와 함께 특징을 살펴보며 공부해도록 하겠습니다.

 

 

 

상속

상속은 OOP에서 가장 기본개념중에 하나입니다. 부모 - 자식 클래스로 정의하여 자식클래스에서 부모 클래스의 메소드를 override, 재사용 할 수 있다는 특징이 있습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;

    public void start() {
        System.out.println("차량이 시동됩니다.");
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

 

public class ElectricCar extends Car {

    private int batteryCapacity;
    private int batteryLevel;

    public ElectricCar(String brand, String model, int year, int batteryCapacity) {
        super(brand, model, year);
        this.batteryCapacity = batteryCapacity;
        this.batteryLevel = 100;
    }

    @Override
    public void start() {
        System.out.println("전기차가 조용히 시동됩니다.");
    }

    public void charge() {
        System.out.println("배터리를 충전합니다.");
        this.batteryLevel = 100;
    }
    
}

 

ElectricCar 클래스는 Car 클래스를 상속받고있습니다. 따라서 Car와 ElectricCar는 부모-자식 관계입니다.

ElectricCar는 Car의 기능을 재사용하거나 새로운 기능을 손쉽게 추가할 수 있고, 때에 따라서 Override 할 수 있습니다. 그리고 Car 에서 변경된 사항은 ElectricCar에도 그대로 반영됩니다.

 

 

 

복합

@AllArgsConstructor
public class Engine {

    private final String type;
    private final int hp;

    public void start() {
        System.out.println(type + " 엔진이 가동됩니다.");
    }

}

 

@AllArgsConstructor
public class GasCar {

    private final Car car; // 복합관계
    private final Engine engine; // 복합관계

    public void start() {
        car.start();
        engine.start();
    }
}

 

Engine 클래스와 GasCar 클래스를 구현했습니다. GasCar는 Car와 Engine을 필드에서 참조하고있습니다.

이처럼 GasCar가 Car를 필드로 가지는 것이 복합관계라고합니다. GasCar를 통해 Car 메소드를 호출하는 방식이죠.

 

 

상속의 장단점

장점

  1. 코드 재사용성
    • 부모 클래스의 기능을 그대로 물려받아 사용할 수 있어 코드 중복을 줄일 수 있습니다.
    • 공통 기능을 부모 클래스에 구현하면 모든 자식 클래스가 활용할 수 있습니다.
  2. 간단한 확장성
    • 기존 클래스를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
  3. 다형성 구현
    • 부모 타입으로 자식 객체를 참조할 수 있어 유연한 프로그래밍이 가능합니다.
Car car = new ElectricCar("Tesla", "Model 3", 2024, 75);

 

단점

  1. 강한 결합도
    • 부모 클래스의 변경이 모든 자식 클래스에 영향을 미칩니다.
    • 부모 클래스의 내부 구현을 자식 클래스가 알아야 할 수 있습니다.
  2. 취약한 기반 클래스 문제
    • 부모 클래스의 변경이 예상치 못한 자식 클래스의 동작 변경을 일으킬 수 있습니다.
  3. 단일 상속 제한
    • Java는 다중 상속을 지원하지 않아 한 클래스만 상속할 수 있습니다.

 

복합의 장단점

장점

 

  1. 유연한 설계
    • 런타임에 구성요소를 변경할 수 있습니다.
    • 다중 복합이 가능합니다.
  2. 캡슐화 향상
    • 내부 구성요소의 구현을 완전히 숨길 수 있습니다.
    • 인터페이스를 통한 통신이 가능합니다.
  3. 느슨한 결합도
    • 각 클래스가 독립적으로 동작하여 유지보수가 용이합니다.
// Engine을 쉽게 교체할 수 있음
public void updateEngine(Engine newEngine) {
    this.engine = newEngine;
}

 

 

 

단점

  1. 코드 가독성
    • 복잡한 구성의 경우 코드 추적이 어려울 수 있습니다.
  2. 구현의 복잡성
    • 여러 클래스를 조합하여 사용하므로 초기 설계가 더 복잡할 수 있습니다.
// 복합 관계에서는 위임 메서드를 많이 작성해야 할 수 있음
public class GasCar {
    private Car car;
    private Engine engine;
    
    public void start() { car.start(); }
    public void stop() { car.stop(); }
    public void accelerate() { engine.accelerate(); }
    // ... 더 많은 위임 메서드들
}

 

 

결론

상속과 복합에 대한 글을 읽다보면 'IS-A' 일때는 상속, 'HAS-A' 일때는 복합을 사용하라는 글이 많습니다.

'전기차는 차다' -> 상속,

'차는 엔진을 갖고있다' -> 복합,

이렇게 이해하면 조금더 와닿을것 같습니다.

 

최근의 객체지향 설계에서는 "상속보다는 복합을 사용하라"는 원칙이 널리 받아들여지고 있습니다. 복합이 더 유연하고 안전한 설계를 제공하기 때문입니다. 하지만 각각의 상황에 맞는 적절한 선택이 중요합니다.

 

마지막으로 위의 예제를 상속과 복합관계를 이용하여 CasCar를 조금 더 깔끔하게 바꿔보면서 글을 마치겠습니다.

 

@AllArgsConstructor
public class Car {

    private final String brand;
    private final String model;
    private final int year;
    private final Engine engine;

    public void start() {
        System.out.println("차량이 시동됩니다.");
        engine.start();
    }

    public void stop() {
        System.out.println("차량이 정지합니다.");
    }

}

public class GasCar extends Car {

    private final int fuelCapacity;
    private double currentFuel;

    public GasCar(String brand, String model, int year, Engine engine, int fuelCapacity) {
        super(brand, model, year, engine);
        this.fuelCapacity = fuelCapacity;
        this.currentFuel = fuelCapacity;
    }

    public void refuel(double amount) {
        currentFuel = Math.min(currentFuel + amount, fuelCapacity);
    }

}

 

차는 엔진을 가지고있다 -> private final Engine engine

가솔린차는 차이다 -> GasCar extends Vehicle

 

Spring Scheduler

스케줄러는 스프링 프레임워크에 포함된 기능입니다. 특정 시간이나 정해진 주기에 따라 작업을 수행하는데 사용하는데 오늘은 이 기능에 대해서 알아봅시다.

 

 

 

사용방법

 

설정

Spring Scheduler를 사용하기 위해서는 @EnableScheduling 어노테이션을 선언해주어야 합니다.

@EnableScheduling
@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

 

 

구현

다음은 스케줄을 구현하면되는데 스프링 빈으로 등록되어야지 동작합니다.

@Component
public class TestSchedule {

    @Scheduled(fixedRate = 1000)
    public void test() {
        System.out.println("TestSchedule");
    }

}

 

 

실행해보면 1초간격으로 동작하는것을 확인할 수 있습니다.

 

 

 

@Scheduled 사용법

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
@Reflective
public @interface Scheduled {
    String CRON_DISABLED = "-";

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1L;

    String fixedDelayString() default "";

    long fixedRate() default -1L;

    String fixedRateString() default "";

    long initialDelay() default -1L;

    String initialDelayString() default "";

    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    String scheduler() default "";
}

 

  1. cron
    • 표현식을 이용해 스케줄을 지정할 수 있습니다. 
    • "초 분 시 일 월 요일" 형식으로 작성합니다.
    • 자세한 설명을 글 하단에서 하겠습니다.
  2. zone
    • cron 표현식에서 사용할 시간대를 지정합니다.
    • default 는 서버의 로컬 시간대입니다.
    • "Asia/Seoul", "UTC" 등
  3. fixedDelay, fixedDelayString
    • 이전 작업의 종료시점부터 다음 작업의 시작까지의 지연시간입니다.
    • default fixedDelay = 1000 : 1초 (이전 작업 완료 후 1초 뒤 실행)
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  4. fixedRate, fixedRateString
    • 이전 작업의 시작지점부터 다음 작업의 시작까지의 시간간격입니다.
    • default fixedRate = 1000 : 1초 (이전 작업 시작으로부터 1초 마다 실행)
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  5. initialDelay, initalDelayString
    • 애플리케이션 시작 후 첫 작업이 실행되기까지의 지연시간을 지정할 수 있습니다.
    • default는 위와 같습니다.
    • 단위는 timeUnit에 따라 변경될 수 있습니다.
  6. timeUnit
    • fixedDelay, fixedRate, initialDelay의 시간단위를 지정합니다.
    • default TimeUnit.MILLISECONDS
  7. scheduler
    • 스케줄러의 이름을 지정할 수 있습니다.
@Scheduled(cron = "0 0 12 * * ?")  // 매일 12시에 실행
public void test1() {
    // 작업 내용
}

@Scheduled(fixedRate = 5000)  // 5초마다 실행
public void test2() {
    // 작업 내용
}

@Scheduled(fixedDelay = 1000, initialDelay = 5000)  // 시작 5초 후 첫 실행, 이후 1초 간격으로 실행
public void test3() {
    // 작업 내용
}
  •  

 

 

 

cron

cron은 "초 분 시 일 월 요일" 형식으로 된 String 문자열입니다.

필드 허용값
0 - 59
0 - 59
0 - 23
1 - 31
1- 12 | JAN-DEC
요일 0 - 6 | SUN - SAT

 

cron 특수문자

특수문자 설명 예제
* 모든 값  
? 특정한 값이 없음 일, 요일에서만 사용가능
- 범위를 나타냄 1-3 : 1월 ~ 3월
, 값을 여러개 나열 MON,SAT,SUN : 월,토,일
/ 시작시간과 단위 분에서 1/5 : 1분부터 5분단위로
L 마지막 값 일, 요일에서만 사용가능
일에서 사용하면 마지막 일,
요일에서 사용하면 마지막 요일(6)
W 가장 가까운 평일 일 에서만 사용가능
1W : 1일에서 가장 가까운 평일
# 몇째주의 무슨요일 0#3 : 3번째 주 일요일

 

 

 

cron 예제

// 매일 자정에 실행
@Scheduled(cron = "0 0 0 * * ?")

// 평일 오전 9시에 실행
@Scheduled(cron = "0 0 9 * * MON-FRI")

// 매월 1일 오전 3시에 실행
@Scheduled(cron = "0 0 3 1 * ?")

// 매주 일요일 자정에 실행
@Scheduled(cron = "0 0 0 ? * SUN")

// 매월 마지막 날 오후 11시 45분에 실행
@Scheduled(cron = "0 45 23 L * ?")

// 매월 마지막 토요일 오후 10시에 실행
@Scheduled(cron = "0 0 22 ? * 6L")

// 매월 첫번째 화요일 오전 9시에 실행
@Scheduled(cron = "0 0 9 ? * 2#1")

// 매일 오전 9시부터 오후 5시까지 30분마다 실행
@Scheduled(cron = "0 0/30 9-17 * * ?")

// 매일 10시 15분에 실행
@Scheduled(cron = "0 15 10 * * ?")

// 매주 월요일 10시에 실행
@Scheduled(cron = "0 0 10 ? * MON")

// 매주 월,수,금 10시에 실행
@Scheduled(cron = "0 0 10 ? * MON,WED,FRI")

// 15분마다 실행
@Scheduled(cron = "0 0/15 * * * ?")

// 평일 9시-18시 정각마다 실행
@Scheduled(cron = "0 0 9-18 * * MON-FRI")

// 매월 마지막날 12시에 실행
@Scheduled(cron = "0 0 12 L * ?")

// 매월 마지막 평일 12시에 실행
@Scheduled(cron = "0 0 12 LW * ?")

 

 

설정분리

schedule.cron=3 * * * * ?
schedule.enabled=true

application.properties에 설정값을 분리하고

 

@Component
public class TestSchedule {

    @Value("${schedule.enabled}")
    private boolean enabled;

    @Scheduled(cron = "${schedule.cron}")
    public void test() {
        if (enabled) {
            System.out.println("TestSchedule");
        }
    }

}

 

이렇게 작성하면 설정부분과 실행부분을 나눌 수 있어서 좋은것 같습니다.

개요

Spring JPA에는 open-in-view 를 true / false 로 설정할 수 있습니다.

spring.jpa.open-in-view=true // true : default

오늘은 이 기능이 무엇을 하는지 간단하게 알아보도록 합시다.

 

 

 

 

OSIV

open-in-view(OSIV) 는 영속성 컨텍스트가 데이터베이스 커넥션을 언제 돌려주는지 설정하는 것입니다.

OSIV의 기본전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 까지 영속성 컨텍스트

데이터베이스 커넥션을 유지하는 것입니다. 즉, 트랜잭션이 종료되어도 데이터베이스 커넥션을 유지하는 것인데요.

이 전략은 Controller에서 지연로딩을 통해 데이터를 받아올 수 있다는 장점이 있습니다.

다만, 이 기본전략은 데이터베이스 커넥션을 필요 이상으로 오래 사용하고, 오래 사용하기 때문에 트래픽이 많은 어플리케이션에서는 커넥션이 모자라 문제를 일으킬 수 있다는 것입니다.

 

 

spring.jpa.open-in-view=false

따라서 OSIV를 false로 하여 커넥션 반환시점을 트랜잭션 종료시점과 맞추어 사용하는 것이 조금더 나을 수 있습니다.

'FrameWork > JPA' 카테고리의 다른 글

[JPA] default_batch_fetch_size  (0) 2024.11.08
JPA 스키마 자동 생성  (0) 2023.11.17

퍼사드 패턴

퍼사드 패턴은 복잡한 클래스들을 편하게 사용하기위해 인터페이스를 구성하는 구조 패턴입니다.

 

 

 

예제

IOT 를 조작하는 컨트롤러를 만든다고 가정해보겠습니다.

컨트롤러는 일어날때 커튼을 열고, 전등을 키고, 스피커에서 음악이 재생되도록 하고싶고,

집을 떠날때는 전등을 끄고, 스키퍼에서 음악이 멈추도록 하고싶습니다.

 

public class Remote {

    private final Lights lights;
    private final Curtains curtains;
    private final Speaker speaker;

    public Remote(Lights lights, Curtains curtains, Speaker speaker) {
        this.lights = lights;
        this.curtains = curtains;
        this.speaker = speaker;
    }

    public void wakeUp() {
        lights.on();
        curtains.open();
        speaker.musicStart();
    }

    public void leaveHome() {
        lights.off();
        speaker.musicStop();
    }
    
}
public class Lights {

    public void on() { ... }
    public void off() { ... }
    
}

public class Curtains {

    public void open() { ... }
    public void close() { ... }
    
}

public class Speaker {

    public void musicStart() { ... }
    public void musicStop() { ... }
    
}

 

이렇게 작성한다면 Remote를 사용하는 사용자는 내부 클래스의 구현에 대해서 알 필요가 없어지고, 내부 클래스의 수정이 일어나도 Remote 코드만 수정하면 되므로 유연성이 향상됩니다.

 

 

이 퍼사드 패턴은 디자인패턴에 대해서 알지 못하더라도 그냥 자연스럽게 만들어지는 패턴같습니다. 쉽고 유용한 패턴이니 잘 적용해보시기 바랍니다.

spring.jpa.properties.hibernate.default_batch_fetch_size

application.properties

 

default batch fetch size

JPA에서 default_batch_fetch_size 는 지연로딩되는 쿼리를 IN 으로 묶어 한번에 보내는 기능입니다. 조회 시 성능을 개선하는 방법중 하나죠.

 

 

 

List<Order> orders = orderRepository.findAll();
orders.forEach(order -> System.out.println(order.getMember().getName()));

예를들어, 위와 같은 로직이 있을때 orders의 사이즈가 100이고, Member는 fetch=LAZY 이라고 가정해보겠습니다.

 

order.getMember().getName()을 호출하면 영속성 컨텍스트에 데이터가 없기때문에 먼저 쿼리가 나가고 영속성 컨텍스트에 저장됩니다. Member의 중복이 없다면 이 과정에서 100번 반복되어 총 쿼리는 최대 101번이 나갈 수 있게되는것입니다.

 

여기에서 default_batch_fetch_size=100 라고 설정해준다면 지연로딩되는 member를 IN절로 100개까지 묶어 한번에 모두 가져와 컨텍스트에 넣게됩니다. 그러면 이 경우에 2번의 쿼리만 발생하게됩니다.

 

따라서 default_batch_fetch_size를 적절하게 설정해놓는것이 좋겠습니다.

'FrameWork > JPA' 카테고리의 다른 글

[JPA] Open Session In View  (0) 2024.11.10
JPA 스키마 자동 생성  (0) 2023.11.17

개요

Flutter에는 부모위젯으로부터 제약을 받아 자신의 크기를 결정하는 위젯들이 존재합니다.

만약 부모의 크기가 정해지지 않다면 자신의 크기를 정하지 못해 오류가 발생합니다. 이에 대해서 알아봅시다.

 

 

Column - Column - (Expanded, Flexible)

Expanded와 Flexible은 부모의 사이즈를 채워주는 위젯입니다.

Expanded나 Flexible를 적용한 모습

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded( // Flexible 로 마찬가지
        child: Container(
          color: Colors.blue,
          child: ...,
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

Column 안에 Expanded를 넣으면 깔끔하게 채워줍니다. 이게 가능한 이유는

  1. Column / Row 는 부모로부터 제약조건을 받아야한다. 없다면 하위 위젯에 크기에 따라서 정해진다.
  2. Scaffold 의 body사이즈는 MediaQuery의 크기(화면 크기)에서 AppBar, BottomNavigationBar등이 차지하는 공간을 제외한 나머지 영역의 크기로 정해져있어 크기 제약조건이 존재한다.
  3. Column의 부모위젯에 제약조건이 존재한다.

따라서 Expanded 위젯을 사용하더라도 오류가 발생하지 않습니다.

 

 

그런데 만약에 Column - Column 안에 Expanded가 들어간다면

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Column(
        children: [
          Expanded(
            child: Container(
              color: Colors.blue,
              child: ...,
            ),
          ),
        ],
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

Column 안에 Column 에 Expanded를 하면 

RenderFlex children have non-zero flex but incoming height constraints are unbounded

오류가 발생합니다.

 

그 이유는

  1. 내부 Column의 크기 제약조건이 존재하지않는다.
  2. 내부 Column의 크기는 그 하위 위젯의 크기에 따라 결정된다.
  3. 내부 Column안에 Expanded 위젯은 부모의 크기제약에 따라 크기가 결정된다.

2번과 3번이 서로 크기 제약조건을 의존하고있는걸 확인할 수 있습니다. 따라서 오류가 발생하는것입니다.

 

이 같은 조건은 Row - Row에서도 동일하게 동작합니다.

 

 

ListView

ListView는 기본적으로 크기제약이 없고 가능한 모든 공간을 사용하려고 합니다. 제 기준 가장 사용하기 까다로운 위젯입니다. 왜냐하면 ListView를 단독으로 사용할일이 거의 없기 때문입니다.

 

보통 SingleChildScrollView 위젯내에서 사용하거나 Column 위젯에서 ListView를 사용하는데 오류가 가장 많이 나더라구요.

 

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) => Text('텍스트$index'),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이렇게 Column 안에 놓는다? Vertical viewport was given unbounded height. 바로 오류납니다.

 

위와 마찬가지로 ListView의 크기제약이 없어서 그렇습니다. 이 같은 상황에 해결법은 Expanded 위젯을 사용하는겁니다.

 

body: Column(
    children: [
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
      Expanded(
        child: ListView.builder(
          itemCount: 10,
          itemBuilder: (context, index) => Text('텍스트$index'),
        ),
      ),
      Container(
        height: 50,
        color: Colors.orange,
        child: ...,
      ),
    ],
),

 

이제 오류가 안납니다. 그런데 보통 body에는 처음에 SingleChildScrollView 를 사용해서 전체 스크롤을 가능하게 해줍니다. 화면크기만으로는 컨텐츠를 몇개 못담으니까요.

 

 

body: SingleChildScrollView(
    child: Column(
      children: [
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
        Expanded(
          child: ListView.builder(
            itemCount: 10,
            itemBuilder: (context, index) => Text('텍스트$index'),
          ),
        ),
        Container(
          height: 50,
          color: Colors.orange,
          child: ...,
        ),
      ],
    ),
  )

 

RenderFlex children have non-zero flex but incoming height constraints are unbounded.

 

 

  1. SingleChildScrollView는 자식(Column)에게 높이 제약을 전달
  2. Column은 이 unbounded 제약을 받은 상태에서 Expanded를 만남, 즉 자식위젯들에게 높이 제약을 전달
  3. Expanded는 남은 공간을 차지하려고 하는데, unbounded 상태에서는 '남은 공간'이 얼마인지 계산할 수 없음
  4. 따라서 오류 발생

네 이럴때는 Expanded를 제거해야합니다. 하지만 여기에서 Expanded를 제거한다고 해결되지 않습니다.

 

Vertical viewport was given unbounded height.

오류가 발생합니다.

 

이도 ListView 첫번째 예제와 똑같이 생겼거든요. 그럼 어떻게 해결해야할까요?

 

shrinkWrap: true,

 

ListView에 속성중에 shrinkWrap 옵션이 있습니다. 해당 옵션은 false가 default인데, 이 옵션이 true가 되면 ListView가 자신의 자식들의 높이 합만큼만 크기를 가지도록 만듭니다. 그 말은 무한한 크기를 가지려는 성질을 제한하는 옵션이라는 겁니다. 이 옵션을 사용하면 SingleChildScrollView - Column - ListView 관계를 해결할 수 있습니다.

 

다만, 단점으로는 모든 자식의 높이를 계산해서 자신의 크기를 정해야 하기때문에 자식이 많을수록 성능저하가 발생할 수 있다는 겁니다. 따라서 사용할때는 주의를 갖고 사용해야합니다.

 

 

데코레이터 패턴

데코레이터 패턴은 객체를 동적으로 기능을 확장하거나 추가할 수 있는 패턴입니다. 특징으로는 객체를 감싸는 방식으로 기능을 추가하는건데요. 말로 설명하는 것보다 예제를 통해 배워보도록 하겠습니다.

 

 

 

예제

커피를 예로 들어보겠습니다.

커피의 종류는 아메리카노, 카페라떼, 등등이 있습니다.

이 커피들에 설탕이나 샷을 추가할 수 있다고 했을때, 어떻게 구현해야 할까요?

 

커피와 추가메뉴를 나누고 거기에서 커피객체에 메뉴를 추가하는 식으로 구현할 수 있을것입니다. 그러나 오늘은 데코레이터 패턴을 사용해상속을 통한 확장이 아닌 객체를 감싸는 방식으로 확장을 해보겠습니다.

 

전체적인 구조는 다음과 같습니다.

 

 

 

Coffee

public interface Coffee {

    String getDescription();
    int getCost();
    
}
public class Americano implements Coffee {

    @Override
    public String getDescription() {
        return "아메리카노";
    }

    @Override
    public int getCost() {
        return 5000;
    }
}

 

Coffee에는 메뉴의 이름을 반환하는 getDescription가 가격을 반환하는 getCost()가 있습니다.

Coffee를 상속받은 Americano를 구현했습니다.

 

 

 

CoffeeDecorator

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public int getCost() {
        return coffee.getCost();
    }

}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 샷 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 500;
    }

}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 설탕 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 300;
    }

}

 

CoffeeDecorator를 상속받아 커피에 추가할 수 있는 옵션인 설탕과 샷 객체를 구현했습니다.

CoffeeDecorator의 구현객체를 보면 멤버변수에 Coffee를 가지고있습니다. 추가옵션은 Coffee가 반드시 필요하기 때문입니다.

 

실행결과

    public static void main(String[] args) {
        Coffee coffee = new SugarDecorator(new ShotDecorator(new Americano()));

        System.out.println(coffee.getDescription());
        System.out.println(coffee.getCost());

        // 아메리카노, 샷 추가, 설탕 추가
        // 5800
    }

 

coffee 변수안에 Decorator를 사용해서 추가옵션을 넣어 줄 수 있었습니다.

이렇게 하면 기존코드를 수정하거나 변경없이도 추가 확장할 수 있습니다.

 

여기서 살짝 아쉬운부분은 CoffeeDecorator에서 Coffee를 이미 Override를 했기때문에 Decorator를 구현할때 Override를 강제하기 않는다는것이었는데요. 그래서 강제할 수 있도록 약간 더 수정해보았습니다.

 

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + getExtra();
    }

    @Override
    public int getCost() {
        return coffee.getCost() + getExtraCost();
    }

    abstract String getExtra();
    abstract int getExtraCost();
    
}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 샷 추가";
    }

    @Override
    int getExtraCost() {
        return 500;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 설탕 추가";
    }

    @Override
    int getExtraCost() {
        return 300;
    }
}

 

CoffeeDecorator에서 getExtra()와 getExtraCost()를 추상화하여 상속받은 클래스에게 구현을 강제하게 했습니다.

이렇게 하니까 더 코드가 깔끔해진것 같습니다.

 

이 데코레이터 패턴은 유연하게 확장이 가능하고, 책임을 분리하고, DIP를 준수하는 등 좋은 장점들이 많으나 코드 복잡성이 증가하고, 순서에 의존하는 단점도 존재합니다.

브릿지패턴

브릿지패턴은 객체의 확장성을 높히기 위한 디자인패턴입니다. 객체를 구현클래스와 추상클래스로 분리하고 추상클래스에서 확장해가는 패턴입니다. 오늘은 브릿지 패턴에 대해서 최대한 쉽게 알아보겠습니다.

 

 

 

 

 

예제

 

리모콘과 디바이스의 관계를 보겠습니다.

리모콘은 어떤 디바이스의 종류가 오든 상관없습니다. 그저 전원을 키거나 끄는 역활을 할 뿐입니다.

디바이스는 어떤 리모콘이 오든 상관없습니다. 신호가 오면 전원을 키거나 끄도록 하면 됩니다.

 

여기서 리모콘은 디바이스를 의존하고있어야 어떤 디바이스에 신호를 보낼 지 결정할 수 있습니다. 따라서 리모콘은 디바이스를 의존해야합니다.

리모콘이 디바이스를 의존하는 것을 bridge(다리) 로 표현한 것이 바로 브릿지패턴입니다.

 

 

 

Remote

public abstract class Remote {

    protected final Device device;

    public Remote(Device device) {
        this.device = device;
    }

    public abstract void power();

}

 

public class BasicRemote extends Remote {

    public BasicRemote(Device device) {
        super(device);
    }

    @Override
    public void power() {
        if (device.isOn()) {
            device.off();
        } else {
            device.on();
        }
    }
}

public class SmartRemote extends Remote {

    public SmartRemote(Device device) {
        super(device);
    }

    @Override
    public void power() {
        if (device.isOn()) {
            device.off();
        } else {
            device.on();
        }
    }

    public void runApp(String app) {
        device.runApp(app);
    }
}

 

 

Device

public interface Device {

    void on();
    void off();
    boolean isOn();
    void runApp(String app);

}

 

public class TV implements Device {

    private boolean on = false;

    @Override
    public void on() {
        on = true;
        System.out.println("TV is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("TV is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {
        if (isOn()) {
            System.out.println("TV Run App " + app);
        } else {
            System.out.println("TV is not On");
        }
    }
}

public class Radio implements Device {

    private boolean on = false;
    @Override
    public void on() {
        on = true;
        System.out.println("Radio is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("Radio is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {

    }

}

public class Computer implements Device {

    private boolean on = false;

    @Override
    public void on() {
        on = true;
        System.out.println("Computer is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("Computer is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {
        if (isOn()) {
            System.out.println("Computer run app : " + app);
        } else {
            System.out.println("Computer is not On");
        }
    }
}

 

 

실행결과

    public static void main(String[] args) {
        Device radio = new Radio();
        Remote basicRemote = new BasicRemote(radio);
        basicRemote.power();

        Device tv = new TV();
        SmartRemote smartRemote = new SmartRemote(tv);
        smartRemote.power();
        smartRemote.runApp("YouTube");

        smartRemote.power();
        smartRemote.runApp("Netflix");

    }
Radio is on
TV is on
TV Run App YouTube
TV is off
TV is not On

 

 

브릿지 패턴은 추상클래스와 구현클래스를 분리할 수 있어서 독립적인 확장이 가능합니다. 그리고 런타임시점에 구현체를 변경할 수 있다는 장점이 있습니다. 다만 이 브릿지패턴도 객체지향언어가 모두 그렇듯 분리하면 할수록 코드 복잡성이 증가하는 단점이 있습니다.

들어가며

String의 불변성 - StringBuilder와 StringBuffer의 차이점을 다뤘습니다. 이번에는 String + 연산에 대해서 조금 더 자세하게 다뤄보고도록 하겠습니다. 이전 글을 보고 오시는것을 추천드립니다.

 

 

[JAVA] StringBuilder, StringBuffer 의 차이점과 주의사항

들어가며이 글에서는 StringBuilder, StringBuffer 차이점뿐만 아니라 사용 이유에 대해서도 다루고있습니다. [JAVA] String의 불변성(immutability)들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고

tmd8633.tistory.com

 

 

 

이 글을 쓰는 이유

지난시간에 String 의 불변성과 StringBuilder에 대해서 알아보았습니다. 이전 글들을 작성하면서 String + 연산이 최적화 되었다는 얘기를 수 없이 보았습니다.

 

        String[] arr = {"2", "3", "4"};
        String str = "1";

        for (String s : arr) {
            str += s;
        }

이렇게 String + 연산을 하게되면 String의 불변성 때문에 "12", "123", "1234"의 메모리가 heap 영역에 저장되고 GC 대상도 증가하게 되는 것은 이제 충분히 이해가 됩니다.

그런데 이 과정을 JDK 5 부터 컴파일 시에 내부적으로 StringBuilder를 사용해서 최적화를 하였고, 완전히 최적화가 되지않아서(매번 StringBuilder객체가 생성되는 문제 등) JDK 9부터 지금까지 StringConcatFactory가 사용되어 + 연산이 완전 최적화 되었다고 했습니다. 이를 실제로 알아보기 위해 bytecode를 까봤습니다.

 

 

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 13 L0
    ICONST_3
    ANEWARRAY java/lang/String
    DUP
    ICONST_0
    LDC "2"
    AASTORE
    DUP
    ICONST_1
    LDC "3"
    AASTORE
    DUP
    ICONST_2
    LDC "4"
    AASTORE
    ASTORE 1
   L1
    LINENUMBER 14 L1
    LDC "1"
    ASTORE 2
   L2
    LINENUMBER 16 L2
    ALOAD 1
    ASTORE 3
    ALOAD 3
    ARRAYLENGTH
    ISTORE 4
    ICONST_0
    ISTORE 5
   L3
   FRAME FULL [[Ljava/lang/String; [Ljava/lang/String; java/lang/String [Ljava/lang/String; I I] []
    ILOAD 5
    ILOAD 4
    IF_ICMPGE L4
    ALOAD 3
    ILOAD 5
    AALOAD
    ASTORE 6
   L5
    LINENUMBER 17 L5
    ALOAD 2
    ALOAD 6
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u0001\u0001"
    ]
    ASTORE 2
   L6
    LINENUMBER 16 L6
    IINC 5 1
    GOTO L3
   L4
    LINENUMBER 19 L4
   FRAME CHOP 3
    RETURN
   L7
    LOCALVARIABLE s Ljava/lang/String; L5 L6 6
    LOCALVARIABLE args [Ljava/lang/String; L0 L7 0
    LOCALVARIABLE arr [Ljava/lang/String; L1 L7 1
    LOCALVARIABLE str Ljava/lang/String; L2 L7 2
    MAXSTACK = 4
    MAXLOCALS = 7
}

 

StringConcatFactory.makeConcatWithConstants 메소드가 실제로 String + 연산에 사용되고 있었습니다.

 

그럼 직접 StringBuilder를 사용하지 않고 String + 연산을 해도 최적화가 되는건가? 라는 생각에 테스트를 진행했습니다.

속도테스트는 100배가량 +연산이 느렸고, 메모리도 낭비되는 것을 확인할 수 있었습니다.

 

그럼 무슨 최적화가 되었다는걸까요? 그 결과를 글로 적고싶어서 이렇게 남깁니다.

 

 

Concat

String의 concat() 메소드를 사용해보셨나요? 앞문자열과 뒷문자열을 합쳐주는 기능을 수행하는 메소드입니다.

StringConcatFactory는 그 concat 메소드처럼 여러 문자열을 하나로 합쳐주는 역할을 수행하는 것이었습니다.

 

String str1 = "1";
String str2 = str1 + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9";

 

JDK5 이전에는 이런 String + 연산을 하면 "12", "123", "1234" ... 이런 데이터가 저장이 되었지만, StringConcatFactory는 이런 연속적인 문자열 합치는 것을 한번에 수행하면서 최적화가 되는 것입니다.

 

   L0
    LINENUMBER 8 L0
    LDC "1"
    ASTORE 1
   L1
    LINENUMBER 9 L1
    ALOAD 1
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u000123456789"
    ]
    ASTORE 2

저는 str1 + "2" + "3' ... + "9" 까지 각각 문자열을 더했지만 이를 StringConcatFactory 에서 "23456789" 문자열로 하나로 합쳐 불필요한 메모리 낭비를 최적화 한겁니다. 그러니까 배열에서 하니씩 꺼내며 + 연산할때에는 최적화가 안되었던것이지요.

 

그러니까 String + 연산 은 절대하지말고 StringBuilder, StringBuffer나 쓰십쇼

들어가며

        int[] arr = {1,2,3,4,5};
        int sum = 0;
        
        // for문
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }

        // 향상된 for문
        for (int i : arr) {
            sum += i;
        }

 

향상된 for문은 JAVA 5에서 추가되었습니다. 이전 for문 보다 가독성이 좋아졌고, 더욱 간결해졌습니다.

이번 글에서는 for문과 향상된 for문에 어떤 차이가 있는지 알아보도록 하겠습니다.

 

 

 

for문

        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }

이 부분에 대해서 bytecode를 확인해보겠습니다.

 

L3
FRAME APPEND [[I I I]
    ILOAD 3        // i 값 로드
    ALOAD 1        // 배열 arr 로드
    ARRAYLENGTH    // 배열 길이를 가져옴
    IF_ICMPGE L4   // i가 배열 길이와 같거나 크면 L4로 점프
L5
    ILOAD 2        // sum 값 로드
    ALOAD 1        // 배열 arr 로드
    ILOAD 3        // i 값 로드
    IALOAD        // 배열 arr[i] 값 로드
    IADD          // sum + arr[i] 계산
    ISTORE 2      // 결과를 sum에 저장
L6
    IINC 3 1      // i++
    GOTO L3       // 다시 조건 검사로 이동

 

바이트코드를 해석해본다면 인덱스 i를 매번 증가시키고, 배열의 해당 인덱스에 접근해서 값을 더하는 방식입니다.

중요한건 인덱스를 통해 값을 가져오는 방식이라는 것입니다. 인덱스 개념이 없는 HashMap 과 같은 데이터에서는 사용할 수 없습니다. 배열구조를 가진 데이터에서 데이터를 로드할 수 있습니다.

 

 

 

향상된 for문

 

 

L3
FRAME FULL [[Ljava/lang/String; [I I [I I I] []
    ILOAD 5        // 인덱스 로드
    ILOAD 4        // 배열 길이 로드
    IF_ICMPGE L4   // 인덱스가 배열 길이와 같거나 크면 L4로 점프
    ALOAD 3        // 배열 arr 로드
    ILOAD 5        // 인덱스 로드
    IALOAD        // 배열 arr[인덱스] 값 로드
    ISTORE 6       // 로컬 변수 6에 저장 (배열 요소)
L5
    ILOAD 2        // sum 값 로드
    ILOAD 6        // 로컬 변수 6(배열 요소) 로드
    IADD          // sum + 배열 요소 계산
    ISTORE 2      // 결과를 sum에 저장
L6
    IINC 5 1      // 인덱스 증가
    GOTO L3       // 다시 조건 검사로 이동

 

여기에서 눈 여겨볼 부분은 로컬 변수 6입니다. 일반 for문은 인덱스를 통해 데이터를 바로 sum에 저장하는 방식이었습니다.

하지만 향상된 for문은 로컬변수6에 arr[인덱스] 를 저장하고 로컬변수6을 sum에 저장하는 방식입니다.

 

왜 이런 번거로운 과정이 추가된걸까요?

 

 

Iterator 안전성

향상된 for문이 좋은점은 배열 뿐만이 아니고 Iterator 에서 동작할 수 있다는 것입니다. Iterable 은 Collection 인터페이스에서 상속 받고 있기때문에 컬렉션(List, Set, Map 등) 에서도 향상된 for문을 동작할 수 있다는 것이죠. 이를 위해 Iterator나 내부적인 인덱스 처리로 컬렉션을 순회하는데, 컬렉션의 상태가 중간에 변경될 가능성을 방지하는 안전한 구조가 필요하기 때문입니다.

 

        List<Integer> arr = List.of(1,2,3,4,5);
        int sum = 0;
        
        // 이 코드는
        for (int i : arr) {
            sum += i;
        }
        
        // Iterator 와 동일하다
        Iterator<Integer> iterator = arr.iterator();
        while (iterator.hasNext()) {
            sum += iterator.next();
        }

 

List의 향상된 for문

   L3
   FRAME APPEND [java/util/List I java/util/Iterator]
    ALOAD 3
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
    IFEQ L4
    ALOAD 3
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
    CHECKCAST java/lang/Integer
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    ISTORE 4
   L5
    LINENUMBER 15 L5
    ILOAD 2
    ILOAD 4
    IADD
    ISTORE 2
   L6
    LINENUMBER 16 L6
    GOTO L3

 

 

List의 Iterator

   L3
    LINENUMBER 13 L3
   FRAME APPEND [java/util/List I java/util/Iterator]
    ALOAD 3
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
    IFEQ L4
   L5
    LINENUMBER 14 L5
    ILOAD 2
    ALOAD 3
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
    CHECKCAST java/lang/Integer
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    IADD
    ISTORE 2
    GOTO L3

 

향상된 for문은 컴파일 시 iterator를 사용합니다.

 

 

재사용과 성능최적화

        for (int i : arr) {
            System.out.println(i);
            sum += i;
        }

for 문 안에서 i의 값을 여러번 사용된다면 어떻게 될까요?

 

   L3
   FRAME FULL [[Ljava/lang/String; [I I [I I I] []
    ILOAD 5
    ILOAD 4
    IF_ICMPGE L4
    ALOAD 3
    ILOAD 5
    IALOAD
    ISTORE 6
   L5
    LINENUMBER 11 L5
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ILOAD 6
    INVOKEVIRTUAL java/io/PrintStream.println (I)V
   L6
    LINENUMBER 12 L6
    ILOAD 2
    ILOAD 6
    IADD
    ISTORE 2
   L7
    LINENUMBER 10 L7
    IINC 5 1
    GOTO L3

 

L3에서 ISTORE 6 에 i 값을 저장하고

L5와 L6에서 ILOAD 6 을 통해 로컬변수 6을 로드해 재사용하고 있는 것을 확인할 수 있었습니다. 로컬변수6 데이터를 저장하므로써 단 한번 배열에 접근하는 것으로 다음의 데이터를 모두 처리할 수 있게되었습니다.

 

 

 

읽기전용

향상된 for문에서 배열이나 컬렉션의 요소를 순회할 때, 해당 요소는 로컬변수에 저장되기때문에 읽기 전용상태로 처리됩니다. 즉 복사본을 사용하기때문에 안전한 처리가 가능해집니다.

 

        int[] arr = {1,2,3,4,5};

        for (int i : arr) {
            i = 100;
        }

 

i 는 로컬변수에서 로드된 데이터이기 때문에 i에 직접 할당할 수 없습니다.

다만 참조타입인 경우 얕은복사로 인해 내부데이터의 변경은 가능합니다.

 

    public class Item {

        public int value;

        public Item(int value) {
            this.value = value;
        }
    }

    public static void main(String[] args) {
        List<Item> items = List.of(new Item(1), new Item(2));

        for (Item item : items) {
            item.value = 100;
        }

        for (Item item : items) {
            System.out.println(item.value); // 100
        }
    }

 

 

일관성

향상된 for문은 모든 배열과 Iterable 객체에서 동작하게 설계되었습니다.따라서 배열이 아닌 다른 컬렉션 객체를 다룰 때도 일관성 있게 순회하기 위해, 배열의 요소를 먼저 임시 변수에 저장하고 그 후에 작업을 수행하는 것이 통일된 방식이라고 할 수 있겠습니다.

 

 

 

 

마무리

향상된 for문은 시작, 끝 인덱스를 지정할 수 없습니다. 무조건 전체 순회만 가능하다는 것이죠.

그리고 순회 중에는 데이터를 삭제가 불가능합니다. 시작할 때 전체 길이를 저장하는데 중간에 전체길이가 달라지면 예외가 발생합니다.

 

일반 for문, 향상된 for문간에는 성능차이가 있을까요? 우리는 위에 bytecode를 직접 확인해봤습니다. 향상된 for문에서 추가적으로 로컬변수를 사용하는 차이가 있지만 이는 무시할 정도이고, 로컬변수 도입으로 인한 재사용성으로 인한 최적화도 무시할 정도라고 생각이 됩니다. 따라서 성능차이는 없다고 생각합니다.

 

 

결론은 향상된 for문은 Iterable 객체를 손쉽게 쉽게 사용하기 위해 만들어졌다! 그리고 컴파일 시에 로컬변수를 사용된다!

+ Recent posts