지금까지 DI가 뭔지 살펴봤습니다. 

그렇다면 실제 코드를 보면서 의존성 문제를 외부 주입으로 해결할 때 장점을 살펴봅시다. 

외부 주입없이 코드를 짠다고 했을 때 어떤 문제가 생길까요?

public class GasEngine {
    public void start() {
        System.out.println("Gas engine starts.");
    }
}

public class ElectricEngine {
    public void start() {
        System.out.println("Electric engine starts.");
    }
}

public class GasCar {
    private GasEngine engine;

    public GasCar() {
        this.engine = new GasEngine(); // GasEngine에 직접적으로 의존하고 있음
    }

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

public class ElectricCar {
    private ElectricEngine engine;

    public ElectricCar() {
        this.engine = new ElectricEngine(); // ElectricEngine에 직접적으로 의존하고 있음
    }

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

 

외부 주입을 하지 않고 엔진을 종류별로 만들려면 각자의 클래스가 무한대로 필요하게 될것입니다. 

하지만 외부주입을 할 수 있다면?

 

public interface Engine {
    void start();
}

public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine starts.");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine starts.");
    }
}

public class Car {
    private Engine engine;

    public Car(Engine engine) { // DI를 통해 의존성 주입
        this.engine = engine;
    }

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

public class Main {
    public static void main(String[] args) {
        Engine gasEngine = new GasEngine();
        Car gasCar = new Car(gasEngine);
        gasCar.startEngine(); // "Gas engine starts."

        Engine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.startEngine(); // "Electric engine starts."
    }
}

 

Car에 대한 Class를 하나로 해결할 수 있게됩니다. 

메인 메서드에서 필요한 엔진을 Car에 넣어서 만들 수 있게됩니다. 

무제한으로 늘어나야하는 클래스를 줄일 수 있는 것입니다. 

 

이렇게된다면 중복되는 코드도 줄일 수 있고

Car클래스를 재활용하기 때문에 코드의 효율도 높일 수 있습니다. 

만약 스마트팩토리 자동차 공장에서 DI없이 자동차를 만들어야하는 코드를 작성한다고 생각하면 개발자가 얼마나 하드코딩을 해야하는 것일까요? 하지만 우리는 DI를 통해 이 문제를 쉽게 해결할 수 있습니다. 

 

 

자바와 스프링을 공부하는 사람들에게 필수적인 코스가 있습니다.

그것은 공식문서를 보는 것입니다. 

https://docs.spring.io/spring-framework/reference/core/beans/dependencies.html

 

여기서 특이한점은 공식문서에서 생성자 주입을 권장하고 있다는 것입니다. 

 

공식 문서에서 나온 설명

위 내용을 해석해보자면 아래와 같습니다. 

 

생성자 기반 DI(Constructor-based DI)와 세터 기반 DI(Setter-based DI) 중 어떤 것을 사용할지는 클래스에 따라 다를 수 있습니다. 일반적으로는 생성자를 사용하여 필수적인 의존성을 주입하고, 세터 메소드나 설정 메소드를 사용하여 선택적인 의존성을 주입하는 것이 좋은 방법입니다. @Autowired 어노테이션을 세터 메소드에 사용하면 해당 프로퍼티가 필수적인 의존성이 되지만, 인자의 프로그래밍적 유효성 검사와 함께하는 생성자 주입이 더 선호됩니다.

스프링 팀은 일반적으로 생성자 주입을 권장하며, 이는 애플리케이션 컴포넌트를 불변 객체로 구현하고 필수적인 의존성이 null이 되지 않도록 보장합니다. 또한, 생성자 주입된 컴포넌트는 항상 완전히 초기화된 상태로 클라이언트(호출) 코드에 반환됩니다. 덧붙여 말하자면, 많은 수의 생성자 인자는 좋지 않은 코드의 표시로 여겨지며, 클래스가 너무 많은 책임을 가지고 있다는 것을 의미하며, 관심사의 적절한 분리를 위해 리팩터링되어야 합니다.

세터 주입은 주로 클래스 내에서 합리적인 기본값을 할당할 수 있는 선택적인 의존성에 대해 사용되어야 합니다. 그렇지 않으면 의존성을 사용하는 코드의 모든 위치에서 null 체크를 수행해야 합니다. 세터 주입의 이점 중 하나는 세터 메소드를 통해 나중에 클래스의 객체를 재구성하거나 재주입할 수 있다는 점입니다. 따라서 JMX MBean을 통한 관리는 세터 주입의 사용 사례로서 흥미로울 수 있습니다.

 

 

특정 클래스에 가장 적합한 DI 스타일을 선택하십시오. 때로는 소스 코드를 가지고 있지 않은 서드파티 클래스와 작업할 때 선택이 강제될 수 있습니다. 예를 들어, 세터 메소드를 노출하지 않는 서드파티 클래스의 경우 생성자 주입이 유일한 사용 가능한 DI 형태일 수 있습니다.

일단 생성자 주입을 권장하는 가장 큰 이유는

1. 불변객체로 구현한다

2. 필수적인 의존성이 null이 되지 않도록 보장한다. 

 

생각해보면 너무 당연한 것 아닐까요?

setter의 불안전성은 언제 어디에서 객체의 값이 바뀔지 모른다는 것입니다.

하지만 생성자는 객체가 생성되는 순간 초기화하기 때문에 final 상수로 보장받을 수 있으며 그로 인해 null값인지 확인할 필요 없습니다. 반드시 상수로 존재하기 때문입니다. 

 

필요한 경우( 스프링팀은 선택적인 의존성이라고 명시) 세터 주입도 필요하기 때문에 잘 알고 있는 것이 좋을것 같습니다.

왜냐하면 초창기에 코드 리팩토링을 하다가 무조건 생성자 주입으로 서비스에 때려박았던 저처럼 순환참조에서 빠져나오지 못하고 처음부터 다시 시작하게 될수도 있기 때문입니다. 눈물없이 말할 수 없는 스토리인데...나중에 기억을 더듬어 풀어보도록 하겠습니다....

스프링에서는 외부에서 의존성을 주입하는 것이 가장 큰 특징입니다.

스프링에서는 왜 외부에서 의존성을 주입하는 방식을 사용할까요?

 

외부에서 주입을 하게되는 경우 느슨한 결합으로 강한 의존성을 줄이고 객체의 변경이 다른 객체에 미치는 영향을 최소화 한다는 특징이 있습니다. 이것은 테스트를 용이하게 하고 코드의 가독성과 유지 보수성을 높여줍니다. 확장성과 재사용성에서도 유용합니다.

하지만 이렇게 이론적인 것들을 들어서는 이해하기가 어렵습니다. 

너무 추상적이죠

 

저 역시 처음 자바를 공부할 때 이론으로만 달달 외우던 것들이었습니다.

그럼 이해를 위해 코드를 한번 작성해 봅시다. 

 

첫번째로 살펴볼 것은 생성자 주입방식입니다.

가장 보편적으로 사용되기도 하고 스프링 공식 문서에서 추천하고 있는 방식이기도 합니다. 

생성자주입 방식은 말 그대로 생성자를 통해 객체를 주입하는 방식입니다.

의존성을 객체를 생성할 때 생성자를 통해 주입한다는 말입니다. 

 

public class ExampleService {
    private Dependency dependency;
    
    public ExampleService(Dependency dependency) {
        this.dependency = dependency;
    }
}

클래스는 인스턴스화를 통해 객체를 만들어내기 위해 존재한다고해도 과언이 아닙니다.

하지만 자바에서는 초기값을 정해줘야 컴파일 단계에서 오류가 나지 않는 특징이 있습니다.

따라서 클래스가 존재할때는 반드시  생성자가 있어야합니다. 

자바는 상냥하게 기본생성자를 만들어주기도하지만 명시적으로 명확하게 하기 위해 기본 생성자라고 할지라도 적는 경우가 있습니다.

 

자동차와 엔진을 예로들어서 생성자 주입을 이해해봅시다.

public interface Engine {
    void start();
}

public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine starts.");
    }
}

엔진을 인터페이스로 만들었습니다.

인터페이스 엔진을 상속받아 가스엔진을 오버라이드로 재정의하였습니다.

이때 엔진을 Car클래스에서 어떻게 의존받는지 적어봅시다. 

public class Car {
    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

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

Car 클래스는 Engine에 의존하고 있으며 생성자를 통해 주입되고 있습니다.

starCar 메서드는 Engine의 start메서드를 호출하여 실행됩니다. 

그럼 메인 메서드에서 어떤식으로 객체가 만들어질 수 있는지 봐볼까요?

 

public class Main {
    public static void main(String[] args) {
        Engine engine = new GasEngine();
        Car car = new Car(engine);

        car.startCar();
    }
}

GasEngine인스턴스를 생성하고 engine변수에 넣어 Car객체를 만들때 참조합니다.

이경우 만들어진 car객체의 startCar() 메서드가 실행되면 "Gas engine starts." 를 확인할 수 있습니다. 

이를 통해 Car 객체가 생성될때 Engine 의존성이 주입되어 차의 엔진이 어떤 종류인지에 따라 startCar메서드의 동작이 결정됩니다.

 

이렇게 생성자 주입 방식은 필요한 의존성을 명확하게 만들고,
객체의 불변성을 보장(final)하는 등 여러가지 장점을 제공하고 있습니다.

하지만 단점도 존재합니다.

생성자 주입의 단점

객체 생성을 할 때 필요한 모든 의존성을 제공해야 하므로, 의존성이 많은 경우에 생성자의 인자가 많아집니다. 이는 코드를 이해하고 유지보수하는데 어려움을 줄 수 있습니다. 또한 순환참조가 발생하여 문제가 발생할 수 있습니다. 순환참조의 경우 대부분 디자인의 문제이기 때문에 처음부터 다시 고민하여 로직을 짜는 것이 좋습니다. 

 

두번째, Setter 주입방식을 살펴봅시다. 

이론적인 장점으로 객체를 생성할 때 한번에 주입하는 생성자 주입과 다르게 객체를 생성한 후에도 의존성을 변경할 수 있어서 상대적으로 유연합니다. 선택적인 의존성에 대해 적합한데 이는 모든 의존성을 제공하지 않고 일부만 설정할 수 있기 때문입니다.

이론만 접근하면 너무 추상적이기 때문에 이전에 사용한 Engine과 Car를 사용하여 다시 예시 코드를 작성해 보겠습니다.

 

public class Car {
    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void startCar() {
        if (engine != null) {
            engine.start();
        } else {
            System.out.println("Engine is not installed.");
        }
    }
}

여기서는 Setter를 통해 의존성을 주입하고 있습니다. 이것만봐서는 객체를 생성한 이후에 의존성을 변경할 수 있다는 것이 잘 와닿지 않을텐데요. 그럼 여기서 메인메서드를 보겠습니다. 

 

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

        // Try to start the car without an engine
        car.startCar(); // This will print "Engine is not installed."

        // Now set the engine and try again
        Engine engine = new GasEngine();
        car.setEngine(engine);
        car.startCar(); // This time, it will print "Gas engine starts."
    }
}

메인 메서드에서 처음에는 엔진이 없는 자동차를 만듭니다.

이때 starCar()메서드를 시작해도 엔진이 없어서 자에 시동이 걸리지 않습니다. 

이때 GasEngine 인스턴스를 생성하고 이를 Car의 세터를 통해 주입합니다. 

그러고나서 다시 starCar()메서드를 실행하면 시동이 걸리는 것을 확인할 수 있습니다. 

 

이때 만약 GasEngine이 아니라 전기엔진, 가솔린엔진, 수소엔진 등 여러가지 엔진 버전을 만들어놓은 뒤 세터로 필요한 엔진을 사용하여 객체를 만들 수 있을 것입니다. 생성자 주입 방식에 비해 유연하다는 점은 바로 이런 점 때문입니다. 

 

 

세번째로 필드 주입 방식을 살펴봅시다. 

필드주입방식은 간단하고 직관적이라는 장점이 있습니다. 

필드 주입방식은 @Autowired를 사용하는것으로 상대적으로 간편합니다.

 

import org.springframework.beans.factory.annotation.Autowired;

public interface Engine {
    void start();
}

public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine starts.");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine starts.");
    }
}

public class Car {
    // 필드 주입
    @Autowired
    private Engine engine;

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

하지만 final로 선언될 수 없기 때문에 불변성을 깨뜨릴 수 있습니다. 또한 주입되는 필드를 명시적으로 확인하거나 변경하기 어렵기 때문에 유지보수와 테스트가 어려울 수 있습니다. 스프링 공식문서에서도 생성자 주입이나 세터주입 방식이 권장되는 이유입니다. 

 

마지막으로 인터페이스 주입 방식을 살펴봅시다. 

인터페이스 주입방식의 장점은 특정 인터페이스를 구현한 클래스만 의존성을 주입받을 수 있도록 제한할 수 있습니다. 의존성을 주입받는 객체가 주입받을 수 있는 의존성의 타입을 명시적으로 알 수 있습니다. 

 

역시 이론적으로는 너무 어려우니 코드를 살펴봅시다. 

public interface Engine {
    void start();
}

public class GasEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gas engine starts.");
    }
}

이 부분은 동일하게 적용하고 자동차가 엔진 주입을 받을 수 있도록 인터페이스를 정의해볼까요?

public interface EngineInjectable {
    void injectEngine(Engine engine);
}

EngineInjectable 인터페이스를 구현하고 이 인터페이스를 구현하는 구현체를 만들어 봅시다. 

public class Car implements EngineInjectable {
    private Engine engine;

    @Override
    public void injectEngine(Engine engine) {
        this.engine = engine;
    }

    public void startCar() {
        if (engine != null) {
            engine.start();
        } else {
            System.out.println("Engine is not installed.");
        }
    }
}

여기서 Car클래스는 EngineInjectable를 통해 Engine의존성을 주입받고 있는 것을 볼 수 있습니다. 

마지막으로 이것을 사용하면 메인 클래스를 만들어봅시다. 

 

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

        car.startCar(); // 엔진이 없어서 시동안걸림

        Engine engine = new GasEngine();
        car.injectEngine(engine);
        car.startCar(); // 가스엔진이 시동걸림
    }
}

 

중요한 것을 한번 더 짚고 집어보자면

생성자 주입의 장점 

1. 객체가 생성될 때 필요한 모든 의존성이 주입되기 때문에 일관성을 보장할 수 있습니다.

2. 의존성이 'final'로 선언될 수 있기 때문에 일단 한번 설정되면 변경되지 않습니다. 이는 불변성을 보장하게 되고 스레드 안전성을 확보할 수 있게 됩니다

 

 

 

스텝1

package sangsok.step01;

public class Ellipse {
    public void drawEllipse(){
        System.out.println("Drwing Ellipse");
    }

}
package sangsok.step01;

public class Line {
    public void drwqLine(){
        System.out.println("Drwing Line");
    }

}
package sangsok.step01;

public class Rectangle {
    public void drawRectangle(){
        System.out.println("Drawing Rectangle~");
    }

}
package sangsok.step01;

public class GraphicEditor {

    private static final int ARRAY_LENTH = 5;

    public static void main(String[] args) {
        Rectangle[] rectangles = new Rectangle[ARRAY_LENTH];
        Ellipse[] ellipses = new Ellipse[ARRAY_LENTH];
        Line[] lines = new Line[ARRAY_LENTH];

        //Generate Shapes 도형 생성
        for(int i = 0; i <ARRAY_LENTH ; i++){
            int randomNumber = (int)(Math.random() * 3);
            if(randomNumber == 0){
                rectangles[i] = new Rectangle();
            }else if(randomNumber == 1){
                ellipses[i] = new Ellipse();
            }else{
                lines[i] = new Line();
            }
        }
        //Drwing Sapes 그림 그리기
        for(int i = 0; i < ARRAY_LENTH ; i++){
            if(rectangles[i] != null){
                rectangles[i].drawRectangle();;
            } else if(ellipses[i] != null){
                ellipses[i].drawEllipse();
            } else{
                lines[i].drwqLine();
            }
        }



    }

}

 

스탭2

package sangsok.step02;

public class Ellipse extends Shape{
    public void drawEllipse(){
        System.out.println("Drwing Ellipse");
    }

}
package sangsok.step02;

public class Line extends Shape{
    public void drwqLine(){
        System.out.println("Drwing Line");
    }

}
package sangsok.step02;

public class Rectangle extends Shape{
    public void drawRectangle(){
        System.out.println("Drawing Rectangle~");
    }

}
package sangsok.step02;

public class Shape {
    public void draw(){
        System.out.println("Drawing Shape");
    }

}
package sangsok.step02;

public class Line extends Shape{
    public void drwqLine(){
        System.out.println("Drwing Line");
    }

}
package sangsok.step02;

public class GraphicEditor {

    private static final int ARRAY_LENTH = 5;

    public static void main(String[] args) {
//        Rectangle[] rectangles = new Rectangle[ARRAY_LENTH];
//        Ellipse[] ellipses = new Ellipse[ARRAY_LENTH];
//        Line[] lines = new Line[ARRAY_LENTH];

        //상속을 통해 하나의 배열로 관리할 수 있다.
        //자식은 부모의 모습을 할 수 있다.
        Shape[] shapes = new Shape[ARRAY_LENTH];


        //Generate Shapes 도형 생성
        for(int i = 0; i <ARRAY_LENTH ; i++){
            int randomNumber = (int)(Math.random() * 3);
            if(randomNumber == 0){
                shapes[i] = new Rectangle();
            }else if(randomNumber == 1){
                shapes[i] = new Ellipse();
            }else{
                shapes[i] = new Line();
            }
        }
        //Drwing Sapes 그림 그리기
        //for each 구문 (배열을 구성하고 있는 타입 : 배열) 향상된 포문
        for(Shape shape: shapes){
            //다운 캐스팅이 되는지 확인
            //draw메서드를 호출하기 위해서는 다운캐스팅을 해야한다. (부모가 가진만큼밖에 없음)
            if(shape instanceof Rectangle){
//                Rectangle rectangle = (Rectangle) shape;
//                rectangle.drawRectangle();
                //위의 두줄을 하나로 합쳐 놓은 것. 필요 없는 변수 선언을 하지 않기 위해
                ((Rectangle)shape).drawRectangle();

            } else if (shape instanceof Ellipse) {
                ((Ellipse)shape).drawEllipse();

            } else if (shape instanceof Line) {
                ((Line)shape).drwqLine();

            }
        }



    }

}

 

스탭3

 

package sangsok.step03;

public class Ellipse extends Shape {
    @Override
    public void draw(){
        System.out.println("Drwing Ellipse");
    }

}
package sangsok.step03;

public class Line extends Shape {
    @Override
    public void draw(){
        System.out.println("Drwing Line");
    }

}
package sangsok.step03;

public class Rectangle extends Shape {
    @Override
    public void draw(){
        System.out.println("Drawing Rectangle~");
    }

}
package sangsok.step03;

public class Shape {
    public void draw(){
        System.out.println("Drawing Shape");
    }

}
package sangsok.step03;

public class GraphicEditor {

    private static final int ARRAY_LENTH = 5;

    public static void main(String[] args) {
//        Rectangle[] rectangles = new Rectangle[ARRAY_LENTH];
//        Ellipse[] ellipses = new Ellipse[ARRAY_LENTH];
//        Line[] lines = new Line[ARRAY_LENTH];

        //상속을 통해 하나의 배열로 관리할 수 있다.
        //자식은 부모의 모습을 할 수 있다.
        Shape[] shapes = new Shape[ARRAY_LENTH];


        //Generate Shapes 도형 생성
        for(int i = 0; i <ARRAY_LENTH ; i++){
            int randomNumber = (int)(Math.random() * 3);
            if(randomNumber == 0){
                shapes[i] = new Rectangle();
            }else if(randomNumber == 1){
                shapes[i] = new Ellipse();
            }else{
                shapes[i] = new Line();
            }
        }
        //Drwing Sapes 그림 그리기
        //다형성을 이용하여 코드가 줄어듦
        for(Shape shape: shapes){
            shape.draw();
        }



    }

}

 

프로젝트의 고려사항

  • 리소스(서버, 다루는 것-공유자원, DB, 마감 기한, 학습비용 등) 고려
  • 기술 선택
  • 프로젝트 구현, 스코프
  • 테스트 코드

내가 바라는 개발자의 모습

  • 커뮤니케이션이 활발한 개발자 (컨벤션, 프로젝트 주요사항- README.md)
  • 생각/사고하는 개발자
  • 더 나아지는 개발자
  • 항상 서비스의 확장성에 대해 고려하는 개발자

그게 프로덕트를 서비스답게 만들어? 그래서 뭐 어따 쓸 건데? **꼭 해야해?**

  • 프로덕트에서 00작업/개선이
  1. 왜 필요한지에 대한 고찰(목적, 주요 사용자 시나리오-서비스 핵심기능)
  2. 어떤 것(지표)을 어떻게(기술, 모니터링 방법, 테스트케이스) 확인할 것인지 고민
  3. 개선 방향에 대한 고민 (원인 파악에 따른 여러가지 방법 고려, 해당 방법에 대한 일반적인 장단점뿐만 아니라 그게 우리 프로젝트에 어떤면이 적합하기 때문에)

이렇게 고민하면서 프로젝트 진행하자

  • 기본 준수 : 공식 문서를 읽는
  • RESTful API :선택이지만 기본) API 문서 자동화 (Swagger, Spring Rest Docs)
  • DB :
    • 정규화 / 비정규화 -> 어떤 게 ORM 으로 개발하기 편할까?
    • 인덱싱
  • DB replication
  • 웹 개발자로 기본
    • HTTP Method / status code 처리 (웹 표준 이해 / 공식 문서를 보고 주요 항목 이해가 가능한가 )
    • 서버 (stateless) - 여러 서버가 있다면 어떻게 아키텍쳐를 짜야할까
    • 3 Tier architecture 맞게 패키지 만들기. (기본적인 설명과 why 방식으로 작성하는지)
  • 협업 - 코드 관리 (나만 볼 수 있는 코드가 아니라 '우리' 코드로 만들기)
    • 코딩 컨벤션 (정적분석도구 lint 를 쓰면 편함)
    • git repo 관리 (프로젝트 소개 - README.md / branch 전략 - github flow)
    • git commit - 누가 어떤 기능을 개발했는가? 히스토리 관리, 버그 트래킹 (commit msg 컨벤션 )
  • 프로젝트 설계
    • 기술 선택 이유 (어떤 어떤 부분을 고려해서 이런 방법을 사용했고 그래서 어떻게 되었다 - 일부 채팅 기능, MSA )
  • 트러블슈팅
    • 어떤 상황이 발생했고, 어떤 어떤 방법을 비교했고, 어떤 이유로 이 방법을 적용해서 무엇을 개선했다
    • 내가 무얼 몰라서 찾아봤더니 00방법(공식문서가 아닌 블로그 등 공식 레퍼런스로 부를 수 없는 것)인 거 같아서 고쳐봤더니 동작했다.(X / 트러블슈팅이 아니라 그냥 몰랐던 것임)
    • 리팩토링
  • 서비스 안정성 (테스트 - 서비스가 제대로 동작하는가? 검증)
    • 서비스 사용시 주요 시나리오는 어떻게 되는가?
    • 단위테스트(코드)
    • API 테스트
    • 만약 10000명의 동시접속자가 있다면 서비스가 어떻게 될까? 만약 십만 건의 데이터가 있다면 신경써야할 부분이 뭘까?
    • 서버 부하 테스트 (여기까지 가능할지는 모르겠는데 만들어둔 자료 [공개] 서버 부하 테스트 Server Load Test
    • 보안 101 (이건 못할 수도 있음 [공개]Product Challenge 101 - Security
  • 채팅과 영상 스트리밍에 기본이 되는 기술에 대한 이해
    • 예를 들면, 웹소켓(채팅)에서는 적어도 웹 소켓과 STOMP 의 구현에서 차이
    • Java,Spring 이나 JPA 사용시에 많이 물어보는 질문
  • Java
    • custom Exception (왜 해야하는지 이유)
    • Entity 설계 어떻게 했는지
    • 왜 라이브러리를 사용하지 않았는가 (Komoran 제외하고 자바 코드로 작성한 이유 등)
  • Spring
    • annotation 를 DI 와 연관시켜서 설명
    • GoF 디자인패턴과 Spring 연관관계
  • JPA
    • 계층 DB
    • 기본 이슈 겪어봤는지 - 정규화/비정규화 연결시켜서 순환참조 등
    • 기본 Spring Data JPA 를 사용할 경우 왜 사용했는지, 두 기술의 차이가 무엇인지
    • (코드레벨 에서 리뷰도 진행함)
@Query(value = "SELECT * FROM test.books WHERE MATCH(AUTHR_NM) AGAINST (:query IN BOOLEAN MODE)", nativeQuery = true)
List<Book> findBooksByAuthor(@Param("query") String query);

불리언 모드로 검색하는 경우 많은 데이터들 중에서 양질의 결과만 불러올 수 있지만 사실 누락되는 책들이 많은 것도 사실이다. 그런데 검색어에 따라서 리턴값이 empty인 경우도 있다. 이럴때는 검색범위를 넓혀 검색결과를 보내주는 방법이 필요하다.

이 경우에 서비스단에서 처음 돌려받은 참조변수의 값을 확인하여 null인 경우 다시 정보요청을 하도록 하면 어떨까?

 

이 방법의 단점은 같은 검색을 쿼리를 두번 보내서 해야한다는 것이다.

사용자 경험을 개선하는 것과 데이터베이스의 리소스를 사용하는 것. 이 둘의 밸런스가 중요한것같다. 

 

추가 작성예정

ngram parser를 통해 한글로된 책 제목 검색속도는 엄청난 개선이 있었다.

그런데 이제 영어 제목으로 된 책 검색을 하는 경우 문제가 생긴다.

단순히 apple로만 검색해도 시간이 엄청나게 걸린다. 

 

문제상황 정의 : ngram은 토큰 사이즈가2라서 영어검색을 할때 서칭 민감도가 높아 검색 시간이 오래걸린다.

해결 방안 고민

1. 프런트나 백엔드에서 정규식으로 들어온 검색어가 영어인지 한글인지 구분한다.

2. 토큰사이즈를 4로 만들 인덱스와 ngram으로만든 인덱스를 만들어서 영어는 토큰사이즈4에서 검색하고 한글은 ngram으로 만든 인덱스에서 검색하게 한다. 

 

이 방법이 데이터를 구분해서 따로 테이블을 만드는것보다 인덱스를 만드는 것이 더 효율적인 방법이 될것같다. 기존의 500만건의 데이터를 나눈다는 것은 비용과 시간이 너무 많이 들어가는 일이다. 

테스트는 내일 한다....

도서 통합검색 API를 만드는데 인덱스를 불러오지 못하는 에러가 발생했다. 

에러 : Can't find FULLTEXT index matching the column list

분명 제목, 작가에 대한 인덱스가 각각 존재하는데 왜 불러오지 못하는걸까?

혹시 두개의 자료를 함께 묶어서 인덱스 작업을 했어야하는게 아닐까?

 

통합검색이라면 보통 포함되어야하는 것이 제목, 작가, 출판사 정도이다. 

일단 테스트를 위해 제목과 작가를 하나로 묶어 인덱스 작업을 하는 것이 가능한지 체크해보기로 했다. 

# 인덱스화 같이 만드는 것이 필요
ALTER TABLE books ADD FULLTEXT INDEX idx_authr_nm_title_nm (AUTHR_NM, TITLE_NM);

두개의 컬럼이 묶여서 인덱스가 되어있지 않으면 컬럼 단순히 더하는 방식으로는 Full-Text Search를 못하는것 같다.

 

에러 : Duplicate entry 'NULL-NULL' for key 'books.idx_authr_nm_title_nm'

FullText 인덱스에는 NULL값을 허용하지 않는다. NULL값을 가진 레코드를 인덱스에 추가하려고 할 때 오류가 발생한다고 한다.

해결방법

1. NULL값을 가진 레코드를 삭제한다.

2. 인덱스 생성시 NULL값을 제외하도록 설정한다. 

 

NULL값을 제외하기 위해 찾아보니 coalesce 함수를 사용해보기로 했다. -> 실패

 

일단 통합검색이 실행되도록 프로토타입을 만들기 위해 네이티브 쿼리로 작성해서 시간을 측정해보기로 했다. 

@Query(value = "SELECT * FROM books WHERE TITLE_NM LIKE %:query% OR AUTHR_NM LIKE %:query%", nativeQuery = true)
List<Book> findAllByAuthorAndTitle(@Param("query") String query);

책제목 : 자바 (ㅇ)

작가 : 남궁성(x)

출판사 : 믿음사(x)

 

자바는 검색결과가 나왔지만 작가와 출판사에 대한 검색결과는 나오지 않았다. 

문제점을 찾지 못해 일단은 JPQL 쿼리로 작성하여 재시도

@Query("select b from Book b where b.title like %:title% or b.author like %:title% or b.publisher like %:title%")
List<Book> findAllByTitleOrAuthorOrPublisher(@Param("query") String query);

책제목 : 자바 (ㅇ)

작가 : 남궁성(x)

출판사 : 믿음사(x)

 

JPA쿼리와 마찬가지로 JPQL역시 자바는 검색결과가 나왔지만 작가와 출판사에 대한 검색결과는 나오지 않았다. 

도대체 뭐가 문제인지 모르겠다.

 

좀 쉬다와서 다시 고민해봐야겠다. 

2023.04.12 오후 06:22

 

왜 제목만 검색되고 작가와 출판사에 대한 정보는 검색하지 못하는가?

 

TEXT관련된 검색은 Full-Text 검색으로 속도가 많이 향상되었다.

그러나 숫자로만 이루어져있는 isbn에 대한 검색이 같은 쿼리로는 검색할 수 없다.

isbn 자료는 b트리 인덱스로 만들어뒀기 때문에 JPQL로 수정하여 검색할 수 있게 하였다.

@Query("SELECT b FROM Book b WHERE b.isbn = :isbn")
List<Book> findBooksByIsbn(@Param("isbn") String isbn);

속도도 나쁘지 않게 나오는 성능을 보여준다.

문제점 : isbn에서 한글자라도 틀리거나 덜 적으면 검색결과가 나오지 않는다.

비슷한 결과라도 나올 수 있게 하는 방법은 뭐가 있을까?

일단 Like절을 활용하여 쿼리를 수정해 적용했다. 

// ISBN 검색 Like사용 (B-tree 인덱스)
@Query("SELECT b FROM Book b WHERE b.isbn LIKE :isbn")
List<Book> findBooksByIsbn(@Param("isbn") String isbn);

Like절을 사용하고나니 검색시간이 100배 이상 걸렸다.

사용자 경험을 개선하기 위해서는 isbn이 제대로 입력되지 않아도 비슷한 결과를 찾아올 수 있어야 한다.

뭐가 문제일까?

 

B-tree 인덱스는 주어진 키 값이 정확하게 일치하는 레코드를 찾는 것을 목적으로 설계되었다. 

따라서 isbn을 정확하게 검색한다면 정보를 보다 빨리 찾아올 수 있지만 일부만 일치하는 경우는 아예 검색결과가 나오지 않게 되는 것이다. 사용자 입장에서는 빠르게 찾아오는 것을 선호할까? 아니면 비슷한 숫자를 입력했을 때에도 비슷한 자료라도 보여주는 것을 선호할까?

인덱스를 사용하는것보다 일반 검색이 더 낫지 않을까?

차이가 미비하지만 검색이 빠른경우도 있고 더 느린경우도 있었다.

인덱스를 지우고 일반 쿼리를 짜서 한번 실험해보았다. 

// ISBN 검색 Like미사용 (B-tree 인덱스)
@Query("SELECT b FROM Book b WHERE b.isbn = :isbn")
List<Book> findBooksByIsbn(@Param("isbn") String isbn);

인덱스 없이 검색하는 것은 정말 오랜 시간이 걸린다.

인덱스 없이 검색하는 것은 정확히 입력하더라도 500만건의 데이터안에서 정확한 자료를 찾아오는 것은 쉽지 않은 일이었다. 

인덱스를 사용하면서도 비슷한 자료를 찾아오기 위해서는 b트리인덱스의 장점을 포기해야한다.

 

그렇다면 다른방식의 인덱싱 방법은 없을까?

1. 해시테이블 : 해시 함수를 이용하여 값을 (Key) 연결하여 저장하는 자료구조다. ISBN 해시 함수에 적용하여 해시 (Hash Value) 계산하고, 해당 해시 값에 대응하는 버킷(Bucket) ISBN 저장하는 방식이다. 

 

ngram 인덱스는 full-text검색만 가능하다

 

실험해보고 싶은것. 토큰사이즈가 크면 속도는 어떻게 변할까? 

innodb_ft_min_token_size = 4

토큰사이즈 변경 후 인덱스 다시 만들기

시간 검색 확인 

1. isbn 정확하게 입력했을 경우

@Query("SELECT b FROM Book b WHERE b.isbn = :isbn")
List<Book> findBooksByIsbn(@Param("isbn") String isbn);

이전과 비슷하다

2. Like를 사용했을 경우

// ISBN 검색 Like사용 (B-tree 인덱스)
@Query("SELECT b FROM Book b WHERE b.isbn LIKE :isbn")
List<Book> findBooksByIsbn(@Param("isbn") String isbn);

토큰 사이즈를 4로 늘리자 유의미한 속도향상이 이뤄졌다.

그렇다면 일부만 입력했을 때도 검색이 가능할까?

검색이 되지 않는다. 어째서...

제목같은 경우는 일부만 일치해도 나왔는데 isbn은 왜 안나오는걸까.

 

문제는 ISBN컬럼이 Long타입으로 되어있어서 토큰사이즈에 맞춰 인덱싱할 수 없다는 것이었다.

토큰사이즈를 4로 변경하고 익덱싱 작업을 해서 그렇게 처리 됐을것이라고 생각했는데 그게 아니었다. 

int나 Long 타입은 두가지로 인덱싱 작업이 가능하다. 

 

Long타입은 b-tree 인덱싱, Hash 인덱스 가능하다.

  • B-tree 인덱스는 범위 검색(Range scan)에 적합
  • HASH 인덱스는 등호 검색(Exact match)에 적합  : 따라서, LONG 타입의 컬럼이 등호 검색을 많이 하는 컬럼이라면, HASH 인덱스를 사용하는 것이 더 효율적일 수 있다.

검색을 위해 Long을 String으로 바꾸는 것이 가능하다.

SELECT CAST(ISBN_THIRTEEN_NO AS CHAR) AS ISBN_THIRTEEN_NO_STR FROM books;

검색에 최적화를 위해서 데이터타입을 변경하는 것은 어떨까?

단점이 있을까?

메모리 부하에 대한 문제점?

서버 사용량의 증가?

그냥 정확하게 입력하게 해서 검색하는게 나을까?

 

선택의 연속이다

 

 

테스트 ISBN 검색 자료

9791185390116

9788968306273

9788954820400

 

 

 

 

 

기본 sql 연습

# 이노디비 + 인덱스 관련 자료


#직접 테이블과 필드를 만들어에 FULLTEXT 인덱스를 추가
CREATE TABLE author (
                        id INT AUTO_INCREMENT PRIMARY KEY,
                        title VARCHAR(255) NOT NULL,
                        author VARCHAR(255) NOT NULL,
                        FULLTEXT (author)
) ENGINE=InnoDB;

ALTER TABLE books ADD FULLTEXT (AUTHR_NM);
ALTER TABLE books ADD FULLTEXT INDEX TITLE_FULLTEXT (TITLE_NM);
ALTER TABLE books ADD FULLTEXT INDEX ISBN_THIRTEEN_NO (ISBN_THIRTEEN_NO);

#B트리 인텍스 (숫자정보는 FULLTEXT로 인덱스화할 수 없음)
CREATE INDEX idx_ISBN_THIRTEEN_NO ON test.books(ISBN_THIRTEEN_NO);


alter table books add FULLTEXT index
    AUTHR_NM_IDX(AUTHR_NM) WITH PARSER ngram;

# ngram passer로 인덱싱하여 테이블에 저장
# [AUTHR_NM_IDX]이 만들어질 테이블 이름
# (AUTHR_NM) 인덱싱할 컬럼
alter table books add FULLTEXT index
    TITLE_NM_test1(TITLE_NM) WITH PARSER ngram;

#이노 db 최소 검색값 확인하기
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';

#이노 db 최소 검색값 재설정하기 > 실행후 mysql 재실행 필요
#콘솔에서 변경 불가 > 직접 파일을 찾아서 수정해야함
SET GLOBAL innodb_ft_min_token_size = 2;

#ngram 파서 토큰 사이즈 확인하기 = 기본사이즈 2
SHOW VARIABLES LIKE 'innodb_ft_ngram_size';

연습2. JPQL 연습

create database test;
use test;
create table memver(id varchar(20) primary key, name Long);

use test;
INSERT INTO member (id, password, username) VALUES (1, 'asdf1234', 'gamdol');


select * from oldbooks;
select TITLE_NM from oldbooks;
select AUTHR_NM from oldbooks;
select IMAGE_URL from oldbooks;

#where 조건을 달아서 검색할 수 있다. 그런데 조건을 여러개 걸고싶으면?
select TITLE_NM, AUTHR_NM,BOOK_INTRCN_CN,ADTION_SMBL_NM from oldbooks
    where ADTION_SMBL_NM < 5000;

# select * from book where TITLE_NM != "쑥 캐는 불장이 딸";
select TITLE_NM from oldbooks where TITLE_NM like '나사렛%';

# 조건을 두개 붙이고싶어
select TITLE_NM from oldbooks
                where TITLE_NM like '%자바%'
                and TITLE_NM like '%정석%';

#자바의 정석은 왜 안나오는가? 그냥 책이 없음
select TITLE_NM from oldbooks
                where TITLE_NM like '자바의 정석';

#특정 컬럼의 중복 데이터 제외하고 가져오기
select distinct TITLE_NM from oldbooks;

#모든 컬럼의 중복 데이터 제외하고 가져오기
select distinct * from oldbooks;

#카운트 세기
select count(*) from oldbooks;

#중복되는 책제목 제외하고 카운트 세기
select count(distinct (TITLE_NM)) from oldbooks;

#제목에 자바가 들어간 책의 작가 불러오기
select AUTHR_NM from oldbooks
                where TITLE_NM like '%자바%';

#제목이 알고싶어. 북테이블에있는. 기준은 10000만원이 가면서 (조건추가) 책제목에 자바가 들어가는거.
#만원이 넘는 자바관련 책
select TITLE_NM from oldbooks where ADTION_SMBL_NM >10000 and TITLE_NM like '%자바';
#만원이 넘는 자바관련 책 숫자세기
select count(TITLE_NM) from oldbooks where ADTION_SMBL_NM >10000 and TITLE_NM like '%자바';

#같지않음 조건 걸기
# select * from orders
# where course_title != "웹개발 종합반";

#범위조건
# select * from orders
# where created_at between "2020-07-13" and "2020-07-15";

#포함조건
# select * from checkins
# where week in (1, 3);

#패턴 문자열 조건 걸어보기
# select * from users
# where email like '%daum.net';
# where email like 'a%': email 필드값이 a로 시작하는 모든 데이터
# where email like '%a' email 필드값이 a로 끝나는 모든 데이터
# where email like '%co%' email 필드값에 co를 포함하는 모든 데이터
# where email like 'a%o' email 필드값이 a로 시작하고 o로 끝나는 모든 데이터

select * from oldbooks where TITLE_NM like '%가나안%'

#테이블 만들기
create table test(id int primary key auto_increment ,count int, value varchar(255));

#가나안이 들어가는 책 카운트하기
SELECT COUNT(*), '가나안'
FROM oldbooks
WHERE TITLE_NM LIKE '%가나안%';

#구해온 값을 새로운 테이블에 넣기
insert into test(count, value)
select count(*), '가나안으로 검색했을때 나온 책 권수'
from oldbooks
where TITLE_NM like '%가나안%';

#group by 연습1 틀렸음
SELECT COUNT(*), '자바'
FROM oldbooks
WHERE TITLE_NM LIKE '%자바%'group by 'kakao';

#group by 연습2 그룹바이에 넣는 컬럼을 기준으로 묶인다.
#group by는 동일한 범주를 갖는 데이터를 하나로 묶어서 범주별 통계를 내주는 것, 같은 제목의 책을 묶어서 권수를 셀 수 있다.
select TITLE_NM, count(*)
from oldbooks
where TITLE_NM like '%스프링%'
group by TITLE_NM ;

select TITLE_NM from oldbooks
                where TITLE_NM like '%요리%';

select * from test.oldbooks where title_nm like '%제주%';
select * from oldbooks b where b.TITLE_NM like '%제주%';

#동일한 범주의 개수 구하기
# select 범주별로 세어주고 싶은 필드명, count(*) from 테이블명
# group by 범주별로 세어주고 싶은 필드명;

연습3. Full-Text Search

#JPQL 쿼리 연습하기

select * from oldbooks b;
select * from oldbooks b where b.TITLE_NM like '%제주%';

#여러개의 컬럼에서 같이 불러오고 싶다면? 어떻게 해야하는가
select * from oldbooks b where b.TITLE_NM like '%자바%';

#order by 검색결과 정렬하는 방법 오름차순 ASC
select b.TITLE_NM from oldbooks b order by b.TITLE_NM ASC;
select * from oldbooks b order by b.TITLE_NM ASC;
select * from oldbooks b order by b.PRC_VALUE ;

SELECT * FROM oldbooks b WHERE b.title_nm LIKE '%자바%' OR b.AUTHR_NM LIKE '%자바%' order by b.PRC_VALUE;

#가격이 null이 아니고 제목에 자바가 들어가는 책 검색
SELECT * FROM oldbooks b WHERE b.PRC_VALUE IS NOT NULL AND b.title_nm LIKE '%자바%';

#널 값인 데이터 지우기
# DELETE FROM Book b WHERE b.title_nm IS NULL
#JPQL에서는 엔티티의 속성 값을 삭제할 수 있지만, 속성 자체를 삭제하는 것은 불가능
# 속성값을 null값으로 설정하면 해당 속성이 삭제된 것과 같은 효과를 얻을 수 있다.

#컬럼 날려버리기
# ALTER table book drop column INTNT_BOOKST_BOOK_EXST_AT;
#  ALTER table book drop column TWO_PBLICTE_DE
#  ALTER table book drop column PORTAL_SITE_BOOK_EXST_AT;

select * from books b where b.TITLE_NM like '%자바%' and b.TITLE_SBST_NM like '%자바%';

#특정 단어 제외하고 찾기
SELECT * FROM books b  WHERE b.TITLE_NM LIKE '%자바%' AND b.TITLE_NM NOT LIKE '%스크립트%';
SELECT * FROM books b  WHERE b.TITLE_NM LIKE '%자바%' AND b.TITLE_NM LIKE '%정석%';
SELECT * FROM books b  WHERE b.TITLE_NM LIKE '%스프링%' and b.TITLE_NM like '%자바%';
SELECT * FROM books b  WHERE b.TITLE_NM LIKE '%스프링%' ;

#group by 와 as, count 사용하기
select TITLE_NM, count(*) as '사랑인가봐' from books b where b.TITLE_NM group by b.TITLE_NM;

SELECT * FROM books b  WHERE b.TITLE_NM LIKE '%자바의 정석%'  ;
SELECT * FROM books b WHERE b.TITLE_NM LIKE '%스프링%' OR b.AUTHR_NM LIKE '%스프링%' ;
select b.AUTHR_NM, b.AUTHR_SBST_NM from books b ;

#직접 테이블과 필드를 만들어에 FULLTEXT 인덱스를 추가
CREATE TABLE author (
                       id INT AUTO_INCREMENT PRIMARY KEY,
                       title VARCHAR(255) NOT NULL,
                       author VARCHAR(255) NOT NULL,
                       FULLTEXT (author)
) ENGINE=InnoDB;

ALTER TABLE books ADD FULLTEXT (AUTHR_NM);
ALTER TABLE books ADD FULLTEXT INDEX TITLE_FULLTEXT (TITLE_NM);
ALTER TABLE books ADD FULLTEXT INDEX ISBN_THIRTEEN_NO (ISBN_THIRTEEN_NO);

#B트리 인텍스 (숫자정보는 FULLTEXT로 인덱스화할 수 없음)
CREATE INDEX idx_ISBN_THIRTEEN_NO ON test.books(ISBN_THIRTEEN_NO);


alter table books add FULLTEXT index
    AUTHR_NM_IDX(AUTHR_NM) WITH PARSER ngram;

# ngram passer로 인덱싱하여 테이블에 저장
# [AUTHR_NM_IDX]이 만들어질 테이블 이름
# (AUTHR_NM) 인덱싱할 컬럼
alter table books add FULLTEXT index
    TITLE_NM_test1(TITLE_NM) WITH PARSER ngram;
#이노 db 최소 검색값 확인하기
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';

#이노 db 최소 검색값 재설정하기 > 실행후 mysql 재실행 필요
#콘솔에서 변경 불가 > 직접 파일을 찾아서 수정해야함
SET GLOBAL innodb_ft_min_token_size = 2;

SELECT * FROM test.books WHERE MATCH(TITLE_NM) AGAINST ('자바의 정석' IN NATURAL LANGUAGE MODE);

#자연어 검색
select *from books
where match(TITLE_NM) against ('자바' IN NATURAL LANGUAGE MODE );

#불리언 검색
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('자바' WITH QUERY EXPANSION);
# 자바 and 프로그래밍
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('+자바 +프로그래밍' IN BOOLEAN MODE);
# 자바 or 프로그래밍
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('자바 | 파이썬' IN BOOLEAN MODE);
# 자바 not JavaScript
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('+자바 -JavaScript' IN BOOLEAN MODE);

#쿼리 확장 검색
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('자바' WITH QUERY EXPANSION);

#and 연산자 사용
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('+자바 +프로그래밍' IN BOOLEAN MODE);

#or 연산자 사용
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('자바 | 파이썬' IN BOOLEAN MODE);

# not 연산자 사용
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('+자바 -JavaScript' IN BOOLEAN MODE);

#Limit 절로 반환하는 데이터 제한하기
SELECT * FROM books
WHERE MATCH(TITLE_NM) AGAINST ('자바' WITH QUERY EXPANSION)
LIMIT 10;



# 불리언 모드
select ISBN_THIRTEEN_NO,TITLE_NM,
       match(TITLE_NM) against ('+총 +균 +쇠' IN BOOLEAN MODE ) as score
from books
where match(TITLE_NM) against ('+총 +균 +쇠' IN BOOLEAN MODE );

select * from books where ISBN_THIRTEEN_NO = 9788970127248;
select * from books where ISBN_THIRTEEN_NO = 9788970127248;

# 검색 점수 스코어로 보기
SELECT *, MATCH(TITLE_NM) AGAINST('가나안') AS score
FROM books
WHERE MATCH(TITLE_NM) AGAINST('가나안');

SELECT MATCH (TITLE_NM) AGAINST ('가나안') FROM books GROUP BY ="가나안" WITH ROLLUP;
SELECT 1 FROM t GROUP BY a, MATCH (a) AGAINST ('abc') WITH ROLLUP;


#자연어 임계값 확인
SHOW VARIABLES LIKE 'ft_min_word_len';
SHOW VARIABLES LIKE 'ft_max_word_len';

 

나중에 지워질까봐 백업용

+ Recent posts