Java Spring Core

스프링 기술 이전의 EJB

초기에 자바 기반 웹 어플리케이션 개발을 위해 EJB을 활용하였다. 하지만 EJB가 가지는 복잡하고, 어려운 개념들은 개발자들로 하여금 새로운 방식으로 어플리케이션을 개발하려고 노렸했다.

EJB로 지배되고 있던 자바 개발 환경의 어두운 겨울을 벗어나 새로운 패러다임으로의 길을 열어준 봄과 같은 프레임워크의 등장이 바로 Java Spring이다.

스프링이란

Spring 이라고 하는 것은 단순히 Spring Framework 와 Spring boot을 일컫는 것이 아니라, spring 내에서도 여러 가지 개발 환경이 존재한다.

  • Essential
    • Spring Framework
      • Core
        • DI Container
        • AOP
        • Event
      • Web
        • MVC
      • ORM …
    • Spring Boot: 스프링 부트를 이용하게 되면 스프링 프레임워크를 이용한 개발 과정을 편하게 진행할 수 있다.
      • 기본적으로 웹서버를 내장하고 있어 추가적인 웹서버가 요구되지 않는다./
      • starter 을 통해 내가 사용하고자 하는 라이브러리에 대한 의존성을 자동으로 해결해서 필요한 패키지들을 자동으로 설치해준다.
      • Monitoring 기능을 제공
    • Java Spring framework 기반의 개발과정에 있어서 요즘은 Spring Boot을 빼놓고는 얘기하지 않는다.
  • Additional
    • Spring Data
    • Spring Session
    • Spring Security
    • Spring Rest Docs
    • Spring Batch
    • Spring Cloud

위와 같이 상황에 따라 필요한 개발환경들을 선택해서 활용할 수 있다.

스프링이 왜 필요할까?

스프링은 기본적으로 자바 기반의 프레임워크이다. 자바가 가지는 가장 큰 특징은 OOP(Object Oriented Programming) 객체지향프로그래밍이라는 것이다. 따라서 자바 프로그래머들은 프로그래밍 과정에서 좋은 객체지향 프로그래밍에 대해서 고민을 하고 설계/구현을 진행해야한다.

스프링프레임워크는 이러한 좋은 객체지향 어플리케이션 개발을 도와주는 프레임워크라고 보면 된다.

좋은 객체지향?

OOP

기본적으로 모든 것을 객체로 분류하고, 객체간에 메세지를 주고받으면서 데이터를 처리한다. 이게 객체지향프로그래밍을 하고자하는 목적이다. 이렇게 모든 개념을 객체로 만들어서 프로그래밍을 수행하게 되면 유연하고 변경에 용이한 코드를 개발할 수 있다.

role_implementation

항상 객체 지향 프로그래밍을 수행하기 위해서는 역할과 구현을 분리시켜서 생각해야한다. 이렇게 하므로써 클라이언트는 역할(interface)만 알면 구현 클래스에 대한 내용을 알 필요없이 역할을 이용해서 클래스를 이용할 수 잇다.

또한, 위와 같이 새로운 차종이 생기더라도 자동차의 근본적인 역할에는 변화가 없고, 새로운 구현 클래스만 추가 시키면 된다.

다형성

이렇게 하나의 역할에 대해 여러개의 구현을 가지는 성질은 다형성이라고 하며, 다형성으로 구현된 프로그램을 실행하게 되면 실행 시점에서 어떤 구현 클래스로 역할을 참조했냐에 따라서 다른 메소드가 실행되게 된다.

polymorphism

아래와 같이 MemoryRepository interface를 구현하는 클래스가 MemberMemoryRepository와 JdbcMemberRepository 이렇게 2개가 있을때,

MemberRepository memberRepository1=new MemoryMemberRepository();
memberRepository1.save();
MemberRepository memberRepository2=new JdbcMemberRepository();
memberRepository2.save();

아래와 같이 구현을 하게 되면, memberRespository1은 MemoryMemberRepository 의 save을 memberRepository2는 JdbcMemberRepository의 save를 실행하게 된다.

이 처럼, 구현 클래스에 따라 메소드가 다르게 실행된다. 이를 이용하게 되면 새로운 방식의 DB 접근을 구현한다 하더라도, 구현 클래스만 새로 만들면 되는 장점을 가지게 된다.

좋은 객체지향 프로그래밍

좋은 객체지향 프로그래밍 설계에는 아래 S.O.L.I.D 원칙이 존재한다.

SRP: 단일 책임 원칙(Single Responsiblity Principle)

하나의 클래스는 하나의 책임만을 가져야한다. 클래스 하나하는 최소한의 기능 수행을 목적으로 설계를 진행해야 변경을 하더라도 추가적으로 변경해야되는 소지가 적어진다.

class Car{
    public void start(){}
    public void move(){}
    public void stop(){}
    public int getOilGauge(){} 
    public void washCar(){}
    public void changeTire(){}
}

이런식으로 Car 클래스가 있다고 가정한다면, 실제 운전자의 경우 start,move,stop,getOilGauge 기능을 사용하고, 세차장 직원은 washCar, 정비공은 changeTire 기능을 이용하게 된다.

만약 여기서 운전자가 수행하는 기능에 수정이 일어나게 되면, 나머지 역할을 담당하는 부분에서도 코드 수정이 동반된다. 이는 SRP에 어긋난 개발방식이다.

class moveCar{
    public void start(){}
    public void move(){}
    public void stop(){}
    public int getOilGauge(){}
}
class washCar{
    public void washCar(){}
}
class repairCar{
    public void changeTire()(){}
}

이런식으로 책임단위를 최소화해야 변경 간에 코드 수정요소가 적다.

OCP: 개방-폐쇄 원칙(Open Close Principle)

개방-폐쇄의 원칙은 확장에는 열려있고, 수정은 닫혀있는 것을 의미한다. 기능 확장에는 제한이 없지만 그렇다고 변경을 해서는 안된다는 말이다.

class MemberRepository{
    public void accessMemory(){}
    public void accessmySQL(){}
}

아래와 같은 클래스가 있다고 가정하자. 내가 만약 데이터 저장을 메모리에 하고자 한다면 MemoryRepository의 accessMemory 메소드를 이용하면 되지만, MySQL에 저장하고자 하면 accessmySQL 메소드를 이용하면 된다.

하지만 만약 여기서 내가 MongoDB에 데이터를 저장하고자 한다면 어떻게 해야할까? MemberRepository에 accessMongoDB라는 메소드를 추가해줘야한다. 이처럼 기능을 확장하기 위해서 클래스를 수정하게 되면 OCP 원칙에 어긋난다.

interface MemberRepository{
    public void access(){}
}
class MemoryMemberRepository implements MemberRepository{
    public void access(){}
}
class MySQLMemberRepository implements MemberRepository{
    public void access(){}
}

이렇게 interface와 class를 분할해서 구현하게 되면 나중에 새로운 방식의 저장매체를 이용한다 하더라고 클래스만 추가시켜주면 된다.

LSP: 리스코프 치환 원칙(Liskov Substitution Principle)

리스코프 치환 원칙은 부모 클래스와 자식클래스가 간에 언제 상호 변경이 가능해야한다는 것을 의미한다. 그렇게 하기 위해서는 자식 클래슨 항상 부모 클래스가 의도한 대로 동작해야한다.

public class Rectangle {
    private int width, height;

    public void setWidth(int width) {
        this.width=width;
    }
    public void setHeight(int height) {
        this.height=height;
    }
    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    public Square(int size) {
        super(size, size);
    }
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
    public int getArea() {
        super.getArea();
    }
}

위와 같이 Rectangle, Square 클래스가 있다고 가정하자

이럴때, Rectangle의 길이를 수정하는 함수를 만든다고 가정하자

Rectangle rectangle = new Rectangle();
rectangle.setWidth(3);
rectangle.setHeight(5);
rectangle.getArea(); //==> 15

Rectangle rectangle = new Square();
rectangle.setWidth(3);
rectangle.setHeight(5);
rectangle.getArea(); // ==>25

아래와 같이 함수를 실행하게 되면 에러가 발생하게 된다. Rectangle class에서는 너비와 높이가 다르다는 가정 하에 동작을 진행하지만, Square는 높이와 너비가 같기 때문에 에러가 발생하게 된다.

interface Shape {
  int getArea():
}
public class Rectangle implements Shape {
    private int width, height;

    public void setWidth(int width) {
        this.width=width;
    }
    public void setHeight(int height) {
        this.height=height;
    }
    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    public Square(int size) {
        super(size, size);
    }
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
    public int getArea() {
        super.getArea();
    }
}

LSP를 지켜주기 위해 부모 클래스와 자식 클래스의 성질이 다른 경우 interface를 활용해서 관리한다.

ISP: 인터페이스 분리 원칙(Interface Segregation Principle)

인터페이스 분리의 원칙은 인터페이스의 기능 단위를 최소화하라는 의미한다. 예를 들어 아래와 같은 interface가 있다고 하자.

interface move{
    public void drive();
    public void fly();
}

public class Car implements move{
    public void drive(){};
    //public void fly(){};
}

public class Airplane implements move{
    public void drive(){};
    public void fly(){};
}

interface를 move와 같이 만들게 되면 fly() 동작이 필요없는 car class에서도 fly()를 구현해야되고 이를 해결하기 위해 interface의 기능 단위를 최소화해준다.

interface move{
    public void drive();
}

interface fly {
    public void fly();
}

public class Car implements move{
    public void drive(){};
}

public class Airplane implements move,fly{
    public void drive(){};
    public void fly(){};
}

DIC: 의존관계 역전 원칙(Dependency Inversion Principle

의존관계 역전 원칙은 추상화에 의존해서 객체를 참조해야함을 의미한다.

class K3 {
    public void drive(){}
}
class Santafe {  
  public void drive(){}
    	
}
class CarController {
  public void drive(model,drive) {
    	if(model=="K3")
        	K3.drive();
        else if(model=="Santafe")
        	Santafe.drive();
  }
}

위와 같이 직접적으로 구현 클래스에 접근해서 메소드를 호출하는 방식으로 설계하면, 나중에 새로운 구현 클래스를 생성하게 되면 Controller 클래스의 분기문을 수정해줘야한다.

public interface DriveController {
    public void drive(){}
}
class K3 implements DriveController {
    public void drive(){}
}
class Santafe implements DriveController{  
  public void drive(){}
    	
}

class CarController {
    private DriveController driveController

    public CarController(Model model){
        driveController=model
    }
    public void drive() {
    	driveController.drive()
  }
}

스프링을 사용하는 이유는 뭘까?

다형성을 통해 SOLID 원칙을 지키면서 설계를 진행할 수 있다. 하지만 아래의 코드를 살펴보자

//MemberRepository memberRepository1=new MemoryMemberRepository(); //기존 코드
MemberRepository memberRepository2=new JdbcMemberRepository(); // 변경코드

위는 MemberRepository를 Memory, DB 접근 하냐에 따라서 객체 참조를 다르게 하는 것이다. 하지만 위에 보면 기존에 memory 접근 방식에서 DB 접근 방식으로 바꾸기 위해서는 클라이언트 코드 수정이 동반된다. 이는 OCP 원칙에 어긋난다.

또한, MemoryRepository라는 interface를 이용해서 추상화에 의존하기도 하지만, client는 DB 접근하는 방식임을 인지하고 구현클래스를 직접 선택하게 되므로 구현 클래스에도 의존하는 점이 보인다. 따라서는 DIP 원칙에 어긋난다.

다형성 만으로 객체지향 프로그래밍을 수행하게 되면 이런 OCP, DIP 원칙은 지킬 수가 없는데 스프링은 DI, DI 컨테이너를 이용해서 해당 원칙들을 지킬 수 있도록 지원해준다.

References

link: inflearn

link:springcore

link:ejb

link:lsp

댓글남기기