의존관계 주입(DI)

의존관계 주입(DI)

제어의 역전(Ioc)과 의존관계 주입

우리가 배우고 있는 IoC는 너무 폭넓게 사용하는 언어입니다. 그래서 스프링을 IoC 컨테이너 라고만 말하면 스프링이 제공하는 기능의 특징을 명확하게 설명할 수 없습니다.

이렇게 명확하지 않고 너무 폭넓게 사용하기 때문에 스프링이 제공하는 IoC방식의 핵심을 짚어주는 의존관계 주입(Dependency Injection) 이라는 단어를 사용해서 부르기로 약속하였습니다.

스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불립니다.


런타임 의존관계 설정

의존관계

우선 의존관계 라는 것은 방향성이라는 것이 있습니다. 의존을 하는 것과 의존을 받는 것 2가지로 나뉘어집니다.

만약 의존한다 라는 것은 의존 대상이 변하면 의존하는 것에 영향을 미친다는 것입니다.

다음과 같이 의존이라는 것은 점선으로 된 화살표로 나타냅니다.

현재는 A가 B에 의존하고 있다는 것을 나타냅니다.

만약, B가 변하면 A에 영향이 그대로 전달됩니다.

의존관계에는 방향성이 있습니다. 서로 의존관계라면 예를들어서 B가 현재 새로운 메서드가 추가되거나 기존 메서드의 형식이 변한다면 A에도 영향을 미쳐서 A도 변화를 시켜야합니다.


UserDao의 의존관계

UserDao 는 현재 ConnectionMaker 라는 인터페이스에 의존하고 있습니다.

그래서 만약, ConnectionMaker 의 메서드가 변하던가 이러한 일들이 일어난다면 UserDao에 영향을 직접적으로 받게 됩니다.

하지만 UserDaoConnectionMaker 를 구현한 클래스인 다른 회사의 Maker 예를들어서 AconnectionMaker 등이 변화할 때는 영향이 없습니다.

원래는 UserDaoConnectionMaker 라는 인터페이스가 없었을 경우는 다른 회사의 ConnectionMaker 로 변화했을 경우에 의존관계 때문에 UserDao 의 코드가 직접적으로 변경이 되어야 했지만 이제는 그렇지 않습니다.

이런식으로 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워질 수 있습니다.

우리가 이런식으로 ConnecitonMaker 라는 인터페이스를 통해서 새로 만들어서 UserDao 는 어떤 회사의 ConnectionMaker 를 사용하는지 알지도 못한채로 사용을 하고있습니다.

그래서 결론적으로 UserDaoConnectionMaker 라는 인터페이스를 사용하고 있기 때문에 UserDao 의 오브젝트가 런타임 시에 사용할 오브젝트(의존 오브젝트 / Dependent Object)가 어떤 클래스로 만든 것인지 미리 알 수가 없습니다.

결론적으로 의존관계 주입이란 구체적인 의존 오브젝트와 그것을 사용할 사용할 주체인 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말합니다.

이러한 의존관계 주입은 다음과 같은 세가지 조건을 충족하는 작업을 말합니다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야합니다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

우리는 현재로 DaoFactory 를 사용해서 DI에서 말하는 제 3의 존재를 설정하였습니다.


UserDao의 의존관계 주입

우리는 UserDao에 적용된 의존관계 주입 기술을 다시 살펴볼 것입니다.

우리가 만든 예전의 UserDao 클래스의 생성자는 다음과 같습니다.

public UserDao(){
    connectionMaker = new AConnectionMaker();
}

이러한 UserDao 의 예전 코드는 저런식으로 AConnectionMaker() 라는 구체적인 클래스를 알고있습니다.

이러한 것은 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는 점이 문제점입니다.

이를 우리는 IoC 방식을 사용해서 UserDaoDaoFactory 라는 제3의 존재에 런타임 의존관계 결정 권환을 위임을 시켰습니다.

이런방식으로 우리는 앞서말한 3가지 조건을 충족시켜서 의존관계 주입을 이용했습니다. 이렇게 만들어진 DaoFactory는 다음과 같은 기능을 합니다.

  • 두 오브젝트 사이의 런터임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재입니다.
  • IoC방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너입니다.
  • 또한, 이를 의존관계 주입을 담당하는 컨테이너라고 볼 수 있고 DI 컨테이너라고 불러도 됩니다.

그래서 결론적으로 DaoFactoryDI 컨테이너라고 부릅니다.

우리는 DI 컨테이너인 DaoFactory 를 통해서 UserDao를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 AConnectionMaker의 오브젝트를 전달합니다.

이런식으로 파라미터 전달을 생성자를 통해서 해줍니다. 그래서 우리는 결론적으로 다음과 같은 코드로 런타임 의존관계를 두 오브젝트간에 만들었습니다.

public class UserDao{
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
}

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 래퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다. 라고 보이기 때문에 이를 의존관계 주입(DI)라고 합니다.


의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아닙니다.

의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용해서 하는 방식도 있습니다.

이러한 것을 의존관계 검색(dependency lookup)이라고 불리는 것도 있습니다. 한번 살펴봅시다.

다음과 같은 UserDao의 생성자 리스트를 만들었다고 합니다.

public UserDao(){
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

이렇게 만든 생성자는 자신이 어떤 기업의 ConnectionMaker 오브젝트를 사용할지 미리 알지 못합니다.

또한, 의존대상은 ConnectionMaker 인터페이스 뿐입니다.

코드를 보면 DaoFactory 가 만들어서 돌려주는 오브젝트와 다이내믹하게 런타임 의존관계를 맺고있습니다.

그래서 IoC 개념을 잘 따르고 있지만 적용 방법이 생성자에서 DaoFactroy 에게 스스로 요청을 하고 있습니다.

지금은 자바코드지만 스프링에서 Application Context를 사용했다고 생각하면 다음과 같이 코드가 바뀔것 입니다.

public UserDao(){
    AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(DaoFactory.class);
    this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

getBean() 이라는 메서드를 사용해서 의존관계 검색을 합니다.

이런식으로 ApplicationContext를 사용해서 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 하기 때문에 우리는 의존관계 검색이라고 합니다.

이렇게 만들어진 의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 가집니다. 또한, IoC 원칙에도 잘 들어맞습니다. 하지만 방법만 다릅니다.

결론적으로 어떤것이 더 낫냐라고 본다면 의존관계 주입이 더 깔끔합니다. 또한, 다른 단점들도 존재합니다.

  • 의존관계 검색을 사용하면 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타나서 성격이 다른 오브젝트에 의존하게 되어 바람직하지 않습니다.
  • 사용자에 대한 DB 정보를 어떻게 가져올건가에 집중하는 UserDao 에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여있으면 어색합니다.

그래서 의존관계 주입 방법을 사용하지만 의존관계 검색 방식을 사용해야할 떄도 있습니다.

앞서서 만들었던 UserDaoTest를 다시 봅시다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

확인해보면 의존관계 검색 방식을 사용한 것을 볼 수 있습니다. 이러한 경우는 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 합니다.

왜냐하면 main() 메서드는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문에 우리는 서블릿이라는 것을 사용하여 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다.

우리가 사용하는 의존관계 검색의존관계 주입은 차이점이 있습니다.

의존관계 검색 방식은 검색하는 오브젝트는 자신이 스프링의 빈일 필요는 없습니다.

위에 코드를 살펴보면 UserDao 는 굳이 getBean()이 아닌 new UserDao() 해서 만들어도 사용해도 됩니다.

의존관계 주입에서는 UserDaoConnectionMaker 사이에 DI가 적용되려면 UserDao 도 반드시 컨테이너가 만드는 빈 오브젝트여야 합니다.

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말아야합니다.


의존관계 주입의 응용

런타임 시에 사용 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점은 무엇인지 확인해봅시다.

우리가 앞에서 오브젝트 팩토리로 구현을 하였는데 이는 DI 방식을 구현한 것입니다. 이러한 DI의 장점을 그대로 얻을 수 있습니다.

  • 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않습니다.
  • 인터페이스를 통해 결합도가 낮은 코드를 만들어 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않습니다.
  • 변경을 통한 다양한 확장 방법에는 자유롭습니다.

우리는 이러한 DI 방식을 스프링을 사용하면 99% 혜택을 볼 수 있습니다. 이러한 DI 방식의 여러가지 응용사례를 살펴봅시다.


기능 구현의 교환

실제 운영에 사용할 데이터베이스는 매우 중요한 자원입니다. 이러한 데이터 베이스는 항상 부하가 많이 가해집니다. 그래서 개발 중에는 절대로 사용하지 말아야합니다.

그래서 이제 개발자의 로컬 DB로 사용한다고 생각해봅시다. 우리는 로컬 DB에 대한 기능이 있는 LocalDBConnectionMaker 라는 클래스를 만들고 제일 처음에 만들었던 초난감 DAO와 같이 사용하고 있다고 생각해봅시다.

이렇게 Local에서 사용을 하고나서 다시 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야한다고 생각해봅시다.

우리는 일단 초난감 DAO방식을 사용하고 있기 때문에 모든 DAO들에는 new LocalDBConnectionMaker() 라는 코드를 사용해서 LocalDBConnectionMaker에 의존을 하고 있을 것입니다.

만약 이제 배포를 해야한다고 생각해봅시다. 그러면 LocalDBConnectionMaker 에서 ProductionDBConnectionMaker 라는 클래스로 변경해줘야 한다고 생각해봅시다. 만약 DAO가 1000개라면 1000개의 코드를 모두 수정해야합니다. 이러한 방식은 매우 끔찍합니다.

만약, 초난감 DAO가 아닌 DI 방식을 적용해서 만들었다고 생각해봅시다.

그러면 DAO는 생성 시점에서 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공을 받습니다.

컨테이너에는 구체적으로 사용할 클래스 이름이 들어있습니다.

그래서 @Configuration이 붙은 DaoFactory를 사용한다고 한다면 개발자 PC에서 DaoFactory 코드를 이전에 만든 방식처럼 만들면 됩니다.

@Configuration
public DaoFactory{
    @Bean
    public ConnectionMaker connectionMaker() {
        return new LocalDBConnectionMaker();
    }
}

이런 방식처럼 만든다면 DAO 클래스에서는 어떤 코드도 변경을 안해도 됩니다. 단지 다음과 같이 변경만 해주면 됩니다.

@Configuration
public DaoFactory{
    @Bean
    public ConnectionMaker connectionMaker() {
        return new ProductionDBConnectionMaker();
    }
}

이런식으로 DB가 개발용 배포용이 나눠져있거나 다른 DB들을 여러개 사용할 경우 이런식으로 DI 컨테이너를 만들어서 모든 DAO가 사용할 수 있도록 DI 해줄 수 있습니다.


부가기능 추가

만약에 우리가 DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 생각해봅시다.

첫 번째 방법으로는 그냥 단순하게 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣을 수 있습니다. 하지만 이런 방식은 너무 비효율적 입니다.

두 번째 방법으로는 DI 컨테이너에서 사용할 수 있는 방법입니다. DI에서는 매우 간단하게 하결할 수 있습니다. 그냥 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면 됩니다.

DI 개념을 쉽게 응용한다면 기존코드를 수정하지 않고 그냥 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의하면 가능합니다. 우리는 CountingConnectionMaker라는 클래스를 리스트 1-30과 같이 만듭니다. 또한, 이 클래스는 ConnectionMaker 인터페이스를 구현해서 만든다고 합니다.

public class CoutingConnectionMaker implements ConnectionMaker{
    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    public Connection makeConnection() thorws ClassNotFoundException, SQLException {
        this.counter++;
        return realConnectionMaker.makeConnection();
    }

    public int getCounter() {
        return this.counter;
    }
}

CountingConnectionMaker 클래스는 현재 ConnectionMaker 를 인터페이스를 implement해서 구현을 하였지만 직접 DB커넥션을 만들지는 않았습니다.

대신 DAO가 DB를 가져오는 makeConnection()에서 DB 연결횟수 카운터를 증가시킵니다.

CountingConnectionMaker 는 DB 연결횟수 카운팅이 관심사여서 이를 끝낸 뒤에는 DB 커넥션이 정의되어 있는 realConnectionMaker 에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려줍니다.

이러한 것은 CountingConnectionMaker도 DI를 받고 이에 대한 오브젝트가 DI를 받을 오브젝트도 ConnectionMaker 인터페이스를 구현한 오브젝트입니다. 결론적으로 우리가 사용하는 AConnectionMaker 같은 실제 DB 커넥션을 돌려주는 클래스의 오브젝트를 받습ㄴ디ㅏ.

이는 CountingConnectionMakerConnectionMakerUserDao 로 가는 것을 볼 수 있습니다.

AConnectionMaker 가 구현하고 있는 ConnectionMaker 라는 인터페이스에만 의존하고 있기 때문에 만약 ConnectionMaker 를 implement 하고 있는 어떤 클래스라도 바꿔치기가 가능합니다. 그래서 CountingConnectionMaker 가 사용이 가능한 것입니다.

우리는 이것만 사용하면 안됩니다. CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 AConnectionMaker 를 호출하도록 만들어야 합니다. 이럴 때도 DI를 사용하면 됩니다.

다음과 같이 CountingDaoFactory 라는 컨테이너를 만들어서 사용할 수 있게 만들 수 있습니다.

@Configuration
public class CountingDaoFactory {
    @Bean
    public UserDao userDao(){
        return new UserDao(connetionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new CountingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker(){
            return new AConnectionMaker();
    }
}

이제 커넥션 카운팅을 위한 실행코드를 만들어 봅시다. DL(의존관계 검색)을 사용해서 CountingConnectionMaker 빈을 가져와서 작성해봅시다.

public class UserDaoConnectionCountingTest {
    public static void main(String[] args) throws ClasNotFoundException, SQLException {
        AnnotaionConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(CountingDaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        // DL 사용
        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("Connection counter의 수는 : " + ccm.getCounter());
    }

}

이런식으로 DAO가 여러개여도 관심사의 분리를 통해서 얻어지는 높은 응집도에서 DI의 장점이 나옵니다.

이러한 DI는 정말 중요하고 좋은 도구입니다. 그래서 우리는 DI를 어떻게 활용해야 할지를 공부해야합니다.


메서드를 이용한 의존관계 주입

지금까지는 UserDao의 의존관계 주입을 위해서 생성자를 사용하였습니다.

그래서 생성자에 파라미터를 만들어서 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주었습니다.

현재까지는 생성자에만 사용했지만 이는 일반 메서드를 통해서 의존 오브젝트와의 관계를 주입해줄 수도 있습니다.


수정자(Setter) 메서드를 이용한 주입

수정자는 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하는 것 입니다. 이러한 수정자는 DI 방식에서 활용하기에 적당합니다.


일반 메서드를 이용한 주입

수정자는 정해진 형태가 있고 파라미터는 한개받게 못받습니다. 만약 이런게 싫다면 일반 메서드를 이용할 수도 있습니다.

하지만 파라미터의 개수가 많아져 비슷한 타입이 여러 개라면 실수하기가 쉽습니다. 하지만, 여러개의 파라미터를 받아서 여러 개의 초기화 메서드도 만들 수 있기 때문에 이런것에 대해서 장점이 존재합니다.

스프링은 보통 메서드를 이용한 DI 방식중에서는 수정자 메서드를 가장 많이 사용했습니다.

수정자 메서드 DI를 사용할 때에는 메서드의 이름을 잘 결정하는 것이 좋습니다.

한번 만들어 봅시다. 우리는 기존 생성자를 제거하고 setConnectionMaker() 라는 메서드를 하나 추가한 뒤 파라미터로 ConnectionMaker 타입의 오브젝트를 받도록 선언합시다.

public class UserDao{
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

이런식으로 만들어 주었다면 DaoFactory의 코드도 함꼐 수정해야합니다.

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setConnectionMaker(connetionMaker());
    return userDao;
}

이러면 의존관계를 주입하는 시점과 방법만 달라지고 결과는 동일합니다. 이뿐만이 아니라 다양한 의존관계 주입 방법도 지원합니다.


XML을 이용한 설정

지금까지 DI 컨테이너인 스프링을 도입하면서 애너테이션을 추가해서 DI를 설정해주었습니다.

우리는 DaoFactory와 같은 자바 클래스로만 이용하여 DI 의존관계 설정정보를 만들었지만 다른 방법도 존재합니다.

우리는 그중에서 XML을 이용하는 방법을 살펴 봅시다.

XML의 장점은 다음과 같습니다.

  • 단순한 텍스트 파일이기 떄문에 다루기 쉽습니다.
  • 쉽게 이해할 수 있고 컴파일과 같은 별도의 빌드작업이 필요가 없습니다.
  • 환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 빠르게 변경사항을 반영할 수 있습니다.
  • 스키마나 DTD를 이용해서 정해진 포맷을 따라 작성되었는지 손쉽게 확인할 수 있습니다.

이제 한번 XML을 통해서 만들어 봅시다.


XML 설정

스프링 Application Context는 XML에 담긴 DI 정보를 활용할 수 있습니다.

우리는 <beans> 를 루트 엘리먼트로 사용해서 사용할 수 있습니다. 이러한 <beans> 안에는 여러개의 <bean> 가 들어갈 수 있습니다.

본래대로면 @Bean 메서드에서는 알 수 있는 빈의 DI 정보는 3가지가 존재합니다.

  • 빈의 이름 : 메서드의 이름입니다.
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스에 이용해서 만들지 결정합니다.
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메서드 등을 통해서 의존 오브젝트를 넣어줍니다. 이는 하나 이상일 수도 있습니다.

XML에서도 마찬가지로 이러한 3가지 정보를 정의할 수 있습니다.


connectionMaker() 전환

목록 자바 코드 설정정보 XML 설정정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean 메서드이름() <bean id=”메서드이름”
빈의 클래스 return new BeanClass(); class=”a.b.c...BeanClass”>

다음처럼 1:1교환으로 만들 수 있습니다. 여기서 <bean> 에 들어간느 class 속성에 지정하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름입니다.

그렇기 때문에 메서드의 리턴 타입을 class의 속성에 넣으면 안됩니다.

이를 변경하면 다음과 같습니다.

@Bean
public ConnectionMaker connectionMaker(){
    return new AConnectionMaker();
}

XML은 다음과 같습니다.

<bean id="connectionMaker" class="springbook... AConnectionMaker"/>

DI 컨테이너는 이러한 XML의 <bean> 태그의 정보를 읽어서 우리가 작성했던 connectionMaker() 메서드 와 같은 작업을 진행합니다.


userDao()전환

이번에는 userDao 를 XML로 변환해봅시다. 보통 수정자 메서드를 사용하는게 왜 스프링개발자들이 선호를 했었냐면 XML로 의존관계 정보를 만들 때 편리하기 때문입니다.

만약 수정자 메서드를 사용하면 setConnectionMaker() 가 있다면 set 을 제외한 connectionMaker 라는 프로퍼티를 가집니다.

XML에서는 <property> 태그를 사용해서 의존 오브젝트와의 관계를 정의합니다. 이는 2개의 속성을 가집니다.

  • name : 프로퍼티의 이름 → 이를 통해서 수정자 메서드를 알 수 있습니다.
  • ref : 수정자 메서드를 통해서 주입해줄 오브젝트의 빈 이름입니다. / DI할 오브젝트도 빈입니다. 그 빈의 이름을 지정해주면 됩니다.

그래서 만약에 다음과 같은 방식으로 UserDaoTest 에서 사용을 한다면 하나씩 확인을 해봅시다.

userDao.setConnectionMaker(connectionMaker());

여기서 connectionMaker()userDao 빈의 connectionMaker() 라는 프로퍼티를 이용해서 의존관계 정보를 주입한다는 것입니다. 그래서 ref의 값이 됩니다.

이를 XML로 고치면 다음과 같습니다.

<bean id="userDao" class="springbook.dao.UserDao">
    <property name="connectionMaker" ref="connectionMaker" />
</bean>

XML의 의존관계 주입정보

이를 모두 전환을 해서 적으면 다음과 같습니다.

<beans>
    <bean id="connectionMaker" class="springbook.user.dao.AConnectionMaker" />
    <bean id-="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="connectionMaker"/>
    </bean>
</beans>

현재는 property 태그의 nameref 가 같지만 이름이 같더라도 어떤 차이가 있는지 구별할 수 있어야 합니다.

  • name 속성은 DI에 사용할 수정자 메서드의 프로퍼티 이름입니다.
  • ref 속성은 주입할 오브젝트를 정의한 빈의 ID 입니다.

보통 둘이 같은 경우가 많습니다. 이러한 프로퍼티의 이름은 보통 주입할 빈 오브젝트의 인터페이스를 따르는 경우가 많습니다.

만약 빈의 이름이 중복되거나 의미를 좀 더 잘 드러낼 수 있는 이름이 있다면 변경을 할때 property 태그의 ref 속성의 값도 함께 변경을 해줘야 합니다.

만약 connectionMaker 에서 myConnectionMaker 로 변경했다고 하면 어떻게 되는지 봅시다.

<beans>
    <bean id="**myConnectionMaker**" class="springbook.user.dao.AConnectionMaker" />
    <bean id-="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="**myConnectionMaker**"/>
    </bean>
</beans>

만약, 인터페이스를 구현한 의존 오브젝트를 여러개 만들어 놓고 골라서 사용하는 경우도 있습니다. 아까전에 로컬 DB를 사용하는것 혹은 배포할 때DB를 사용하는것 이러한 기능들이 있는 경우도 있습니다. 그럴때는 다음과 같이 작성을 합니다.

<bean>
    <bean id="**localDBConnectionMaker**" class="...LocalDBConnectionMaker" />
    <bean id="testDBConnectionMaker" class="...testDBConnectionMaker"/>
    <bean id="productionDBConnectionMaker" class="...productionDBConnectionMaker"/>

<bean id-="userDao" class="springbook.user.dao.UserDao">
            <property name="connectionMaker" ref="**localDBConnectionMaker**"/>
    </bean>
</bean>

이런식으로 사용하고 ref 만 내가 사용할 것을 놓으면 됩니다. 바뀌면 이것도 바꾸면 됩니다.


XML을 이용하는 애플리케이션 컨텍스트

이제 Application Context가 DaoFactory 대신 XML 설정정보를 활용하도록 만들어 봅시다.

XML을 사용하면 IoC/DI 작업에서는 GenericXmlApplicationContext 를 사용해서 합니다. 이에대한 생성자 파라미터로 XML 파일의 클래스패스를 정하면 됩니다. 클래스패스를 정해야하기 때문에 보통은 클래스패스 최상단에 둡니다.

이러한 설정파일을 만들 떄는 보통 applicationContext.xml 로 만들어 저장합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="connectionMaker" class="springbook.user.dao.AConnectionMaker"/>

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="connectionMaker"/>
    </bean>
</beans>

이제 UserDaoTest를 수정해봅시다. 이제 AnnotationConfigApplicationContext 대신에 GenericXmlApplicationContext를 사용합니다.

ApplicationContext context = new GenericXmlApplicationContext(
        "applicationContext.xml");

이런식으로 바꿔서 사용하면 됩니다.

또한, GenericXmlApplicationContext 가 아닌 ClassPathXmlApplicationContext 도 존재합니다 이는 XML 파일을 클래스패스에서 가져올 때 사용할 수 있는 편리한 기능이 추가된 것입니다.

보통 XML 파일의 경로를 가져와야하는데 귀찮으면 자동으로 클래스 오브젝트를 사용해서 다음과 같이 변환을 할 수 있습니다.

new GenericXmlApplicationContexct("springbook/user/dao/daoContext.xml");

new ClassPathXmlApplicationContext("daoContext.xml", UserDao.class);

DataSource 인터페이스로 변환

DataSource 인터페이스 적용

우리는 ConnectionMaker 를 만들어 DB 커넥션을 생성해주는 기능 하나만을 정의한 매우 단순한 인터페이스를 만들었습니다.

하지만 사실 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 Datasource 라는 인터페이스가 이미 존재하고 있습니다.

그래서 우리는 ConnectionMaker 와 같은 인터페이스를 만들어서 사용할 일은 없습니다.

Datasource 에서 getConnection() 메서드는 우리가 만든 makeConnection() 과 동일한 기능을 하는 메서드입니다. 이를 통해서 DB 커넥션을 가져올 수 있습니다.

DataSource를 이용해서 한번 UserDao를 리펙토링 해봅시다.

import javax.sql.DataSource;

public class UserDao{
    private DataSource dataSource;

    public void SetDataSource(DataSource dataSource){
        this.dataSource = dataSource;
    }

    public void add(User user) throws SQLException{
        Connection c = dataSource.getConnection();
  }
}

구현을 하였는데 우리는 DataSource 구현 클래스가 필요합니다. 우리는 스프링에서 제공하는 테스트환경에서 간단히 사용할 수 있는 SimpleDriverDataSource 라는 것을 사용해서 이 클래스를 사용하도록 DI를 재구성하려고합니다.


자바 코드 설정 방식

먼저 DaoFactory의 설정방식을 이용해봅시다. 기존의 connectionMaker() 메서드를 dataSource()로 변경하고 SimpleDriverDataSource의 오브젝트를 리턴하게 합니다.

// DaoFactory
@Bean
public DataSource dataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

    dataSource.setDriverClass("com.mysql.jdbc.Driver.class");
    dataSource.setUrl("jdbc:mysql://localhost/springbook");
    dataSource.setUsername("spring");
    dataSource.setPassword("book");

    return dataSource;
}

이제 DaoFactoryuserDao() 메서드도 수정해봅시다.

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    **userDao.setDataSource(dataSource());**
    return userDao;
}

이런식으로 connectionMaker()dataSource() 로 변경하였습니다.


XML 설정 방식

먼저 id가 connectionMaker<bean> 을 없앤 뒤 dataSource 라는 이름의 <bean>을 등록합니다. 그리고 SimpleDriverDataSource 로 변경합니다.

<bean id="dataSource"
    class="org.springframwork.jdbc.datasource.SimpleDriverDataSource"/>

이런식으로 SimpleDriverDataSource의 오브젝트를 만드는 것까지 가능하지만 datasource() 메서드에서 SimpleDriverDataSource 의 오브젝트의 수정자로 넣어준 DB접속정보는 없습니다.

UserDao 처럼 다른 빈에 의존하면 그냥 property 태그랑 ref 속성을 사용해서 의존할 빈의 이름을 넣어주면 되었는데 datasource() 인경우는 어떤식으로 해결을 해야할까요?


프로퍼티 값의 주입

값 주입

DaoFactorydatasource() 메서드에서 본것 처럼 수정자 메서드에는 다른 빈이나 오브젝트 뿐만 아니라 스트링 같은 단순 값을 넣어줄 수도 있습니다.

@Bean
public DataSource dataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

    dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
    dataSource.setUrl("jdbc:mysql://localhost/springbook");
    dataSource.setUsername("spring");
    dataSource.setPassword("book");

    return dataSource;
}

이중 setDriverClass() 메서드는 Class 타입의 오브젝트를 넣었지만 다른 빈 오브젝트를 DI 방식으로 가져와서 넣는 것은 아닙니다.

이렇게 사용하는 수정자 또한, <property>를 사용해서 값을 주입할 수 있습니다. 성격은 다르지만 일종의 DI라고도 볼 수 있습니다.

dataSource.setDriverClass("com.mysql.jdbc.Driver.class");
dataSource.setUrl("jdbc:mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");

이렇게 된 코드들을 XML을 사용해서 DB 연결정보를 설정할 수 있습니다.

<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>

다음과 같은 ref 대신 value 를 써서 나타낼 수 있습니다.


value 값의 자동 변환

그런데 우리는 이상한 점을 볼 수 잇습니다. url, username, password 들은 모두 스트링 타입이기 때문에 텍스트로 정의되는 value 속성 값을 사용하는 것은 문제가 되지 않습니다.

하지만 현재 driverClass 를 보면 java.lang.Class 타입인것을 볼 수 있습니다.

텍스트로 정의되어 있다고 해쓴ㄴ데 별다른 타입정보가 없이 그냥 클래스이름이 텍스트형태로 현재 value에 들어가 있습니다.

직관적으로만 보면 현재 다음과 같은 상황입니다.

Class driverClass = "com.mysql.jdbc.Driver";

이러면 본래 스트링 값을 Class에 넣는것 이기 때문에 컴파일조차 안되기 마련입니다.

하지만 우리는 컴파일을 하면 아무런 문제 없이 성공하는 것을 볼 수 있습니다. 이러한 이유는

스프링이 프로퍼티의 값을 수정자 메서드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해줍니다.

"com.mysql.jdbc.Driver" 라는 텍스트 값을 오브젝트로 자동 변환을 시켜줍니다. 결론적으로는 다음과 같은 코드로 스프링이 자동적으로 변환작업을 해주는 것을 알 수 있습니다.

Class driverClass = Class.forName("com.mysql.jdbc.Driver");
dataSource.setDriverClass(driverClass);

이런식으로 스프링은 value에 지정한 텍스트 값을 적절한 자바 타입으로 변환해주기도 합니다.

결론적으로 우리는 모두 적용하면 다음과 같은 코드를 얻을 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dataSource"
            class="org.springframwork.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost/springbook"/>
        <property name="username" value="spring"/>
        <property name="password" value="book"/>
    </bean>

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

제어의 역전(IOC)

오브젝트 팩토리

현재까지 우리는 DAO를 깜끔한 구조로 리팩토링 하였습니다.

하지만 UserDaoTest는 그냥 넘겼습니다. 현재는 UserDaoTest에서 Connection을 할 때 어떤 클래스를 사용하는지 정의를 하면서 UserDao가 담당하던 기능을 분리하였습니다.

그러나 원래 UserDaoTest를 만든 목적은 UserDao의 기능이 잘 동작하는지를 테스트하기 위해서 만든 것입니다. 그렇긱 때문에 이러한 관심사가 겹치는 것을 분리해보도록 하겠습니다.


팩토리 (Config)

분리시킬 기능을 당담하는 클래스를 만듭니다.

이 클래스는 객체의 생성방법을 결정하고 그렇게 만들어진 Object를 돌려주는 역할을 합니다.

이런것을 우리는 팩토리라고 부릅니다.

이제 DaoFactory라는 클랙스를 생성하고 UserDaoConnectionMaker 관련 생성작업을 옮기도록 합시다.

public class DaoFactory{
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new AConnectionMaker(); // A회사것을 쓰겠다.
        UserDao userDao = new UserDao(connectionMaker);
        return UserDao;
    }
}

이제 DaoFactory 클래스의 userDao() 라는 메서드를 호출한다면 A회사의 AConnectionMaker를 사용해 DB 커넥션을 가져오도록 설계된 UserDao를 반환하게 됩니다.

이제 그러면 UserDaoTest 에서 객체를 설정해주면서 생성해줄 필요가없기 때문에 다음과 같이 고칩니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{
                UserDao dao = new DaoFactory().userDao();
        }
}

설계도로서의 팩토리

결론적으로 이렇게 분리를하게 된다면 DaoFactory 라는 클래스에서 Client가 요청을 한다면

알아서 내가 어떤 것을 Connection으로 사용할지 생성을하고 UserDao에서 그 클래스를 사용하도록 해주게 된다.

이제 회사에 UserDao를 납품할 때 DaoFactory도 함께 납품을 해서 클래스 변경이 일어나면 DaoFactory의 코드만 수정하면 됩니다.


오브젝트 팩토리의 활용

만약, DaoFactory 에서 UserDao 라는 Dao 클래스가 아닌 다른 클래스를 넣는다면 어떻게 될까? 이런식으로 DAO가 늘어나게된다면 Parameter로 넘길 때 ConnectionMaker 의 인스턴스를 생성해서 넘기는 코드가 중복되게 됩니다.

public class DaoFactory{
    public UserDao userDao(){
            return new UserDao(new AConnectionMaker());
    }

    public AccountDao AccountDao(){
            return new AccountDao(new AConnectionMaker());
    }

    public MessageDao messageDao(){
            return new MessageDao(new AConnectionMaker());
    }
}

이런식으로 Parameter에 사용한 ConnectionMaker 를 생성해주는 코드가 중복이 됩니다.

이를 해결하는 제일 좋은 방법은 또 부리하는 것 입니다.

ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메소드로 뽑아내는 것이 좋은 결정입니다.

public class DaoFactory{
    public UserDao userDao(){
            return new UserDao(connectionMaker());
    }

    public AccountDao AccountDao(){
            return new AccountDao(connectionMaker());
    }

    public MessageDao messageDao(){
            return new MessageDao(connectionMaker());
    }

    public ConnectionMaker connectionMaker(){
        return new AconnectionMaker();
    }
}

제어권의 이전을 통한 제어관계 역전

제어의 역전(IOC)가 우리가 배우고자 하는 목표입니다.

제어의 역전은 간단하게 프로그램의 제어 흐름 구조가 뒤바뀌는 것을 말합니다.

보통으로는 main() 메서드에서 다음과 같은 흐름을 가집니다.

  1. 시작되는 지점에서 다음에 사용할 오브젝트를 결정한다.
  2. 결정한 오브젝트를 생성한다.
  3. 만들어진 오브젝트에 있는 메서드를 호출한다.
  4. 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정한다.
  5. 이 과정을 반복한다.

그래서 우리가 초기에 만든 UserDao의 흐름을 살펴보면 다음과 같습니다.

  1. UserDao 클래스의 오브젝트를 직접 생성합니다.
  2. 만들어진 오브젝트의 메서드를 사용합니다.
  3. UserDao는 자신이 사용할 ConnectionMaker의 구현클래스를 자기가 결정합니다.
  4. 그 오브젝트를 필요한 시점에 생성합니다.
  5. 각 메서드가 이를 사용합니다.

우리는 이러한 제어 흐름의 개념을 거꾸로 뒤집어서 사용할 것 입니다.

이를 사용하면 이제 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않습니다.

왜냐하면 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문입니다.

현재 우리는 DaoFactory를 만들어서 제어의 역전(IOC)를 도입하였습니다.

원래는 ConnectionMaker의 구현클래스의 결정은 UserDao에서 하였지만 현재는 이런 구현클래스의 결정에 대해서 DaoFactory에 넘겼습니다.

넘김으로써 UserDao는 능동적이 아닌 DaoFactory가 주는 것을 사용하는 수동적인 존재가 되었습니다.

우리는 현재 IOC프레임워크를 사용하지 않고 DaoFactory를 이용해서 IoC를 구현한 셈입니다.

물론 DaoFactory 같이 Ioc를 만들어도 되지만 프레임워크의 도움을 받는 편이 유리하기 때문에 프레임워크를 사용할 예정입니다.


스프링의 IoC

오브젝트 팩토리를 이용한 스프링 IoC

애플리케이션 컨텍스트와 설정정보

우리는 DaoFacotory를 스프링에서 사용이 가능하도록 변신시킬 것 입니다.

일단 들어가기전에 빈(Bean) 이라는 중요한 개념이 존재합니다.

  • (Bean)
    • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 말합니다.
    • 자바 빈 또는 엔터프라이즈 자바빈에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말합니다.
    • 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말입니다.

또한, 우리는 이러한 빈(Bean)의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 합니다.

하지만 이러한 빈 팩토리보다 Application context를 자주 사용하게 됩니다. 이것은 IoC 방식을 따라 만들어진 일종의 빈 팩토리 입니다.


DaoFactory를 사용하는 애플리케이션 컨텍스트

DaoFactory를 스프링의 빈 팩토리가 사용할 수 있는 본격적인 설정정보로 만들어봅시다.

우리는 @Configuration 이라는 애노테이션을 사용해서 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식 시킵니다.

그리고 @Bean 이라는 애노테이션을 오브젝트를 만들어주는 메서드를 붙여줍니다.

@Configuration // 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
public class DaoFactory{

    @Bean // 오브젝트 생성을 담당하는 IoC용 메서드라는 표시
    public UserDao userDao(){
        return new UserDao(connecitonMaker());
    }

    @Bean
    public ConnectionMakerr connectionMaker(){
        return new AConnectionMaker();
    }
}

이제 설정을 완료했으니 DaoFactory 를 설정정보로 사용하는 Application Context를 만들어 볼 것입니다.

@Configuration 이 붙은 자바 코드를 설정정보로 사용하기 위해서는 AnnocationConfigApplicationContext 를 이용하면 됩니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

우리는 ApplicationContextgetBean() 이라는 메서드를 통해서 UserDao의 오브젝트를 가져올 수 있습니다.

이런식으로 @Bean 이라는 애노테이션을 붙인 매서드가 바로 빈의 이름이 됩니다.

그래서 이름에 따라서 getBean("메서드이름", 사용클래스.class)로 가져올 수 있습니다.


애플리케이션 컨텍스트의 동작방식

기존의 오브젝트 팩토리를 이용한 방식과 스프링 애플리케이션 컨텍스트를 사용한 방식을 비교해봅시다.

스프링에서는 ApplicationContext라는 인터페이스를 통해서 애플리케이션 컨텍스트를 구현합니다.

이러한 ApplicationContextBeanFactory 인터페이스를 상속합니다. 그래서 다음과 같이 불립니다.

  • IoC 컨테이너
  • 스프링 컨테이너
  • 빈 팩토리
  • 스프링

이전에 구현한 DaoFactoryuserDao 를 비롯한 DAO를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 합니다.

하지만 ApplicationContext를 사용하면 DaoFacotory와 달리 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 존재하지 않습니다. 이러한 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻습니다.

그래서 다음과 같은 순서로 작동하게 됩니다.

  1. Client가 UserDao 를 요청합니다.
  2. ApplicationContext 에서 getBean() 메서드를 통해서 생성을 요청합니다.
  3. @Configuration 이 붙은 DaoFactory 에서 @Bean 으로 등록된 UserDao() 메서드를 호출해서 객체를 생성합니다.
  4. 생성한 객체를 Client에게 넘겨줍니다.

이렇게 구현을한다면 다음과 같은 장점이 생깁니다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
    • 애플리케이션 컨텍스트를 사용한다면 오브젝트 팩토리가 아무리 많아져도 이를 알아야하거나 직접 사용할 필요가 없습니다. 또한, XML을 사용해서 컨텍스트가 사용할 IoC 설정정보도 만들 수 있습니다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다.
    • 애플리케이션 컨텍스트가 단지 오브젝트 생성과 다른 오브젝와의 관계설정을 해준다고 생각할 수도 있다.
    • 오브젝트가 만들어지는 방식 오브젝트에 대한 후처리 등등 다양한 기능을 제공하기도 합니다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다.
    • 애플리케이션의 getBean() 메서드를 보면 빈의 이름을 이용해서 빈을 찾아 줄 수 있습니다. 또한, 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있습니다.

스프링 IoC의 용어 정리

빈(Bean)

빈은 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻입니다.

이를 관리 오브젝트(Managed object) 라고도 부릅니다. 하지만 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 아닌 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고합니다.


빈 팩토리(Bean Factory)

빈 팩토리는 스프링의 IoC를 담당하는 핵심 컨테이너를 가리킵니다

빈 팩토리는 다음과 같은 기능을 합니다.

  • 빈을 등록한다.
  • 빈을 생성한다.
  • 빈을 조회후 반환한다.

보통은 빈 팩토리를 바로 사용하지 않고 상속받은 Application Context를 이용합니다.


애플리케이션 컨텍스트(Application Context)

애플리케이션 컨텍스트는 빈 팩토리를 확장한 IoC컨테이너 입니다.

빈 팩토리와 기본적인 기능은 비슷하지만 추가적인 기능들이 확장되어 있습니다.


설정정보/설정 메타정보 (Configuration metadata)

스프링의 설정정보는 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해서 사용하는 메타정보를 말합니다.

IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용됩니다.

이를 Blueprints(청사진)이라고도 합니다.


컨테이너 또는 IoC 컨테이너

애플리케이션 컨텍스트나 빈 팩토리를 말합니다.

이는 애플리케이션 컨텍스트보다는 추상적인 표현입니다.


스프링 프레임워크

스프링 프레임워크는 IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 떄 주로 사용합니다. 주로, 스프링이라고 주여서 말하기도 합니다.


싱글톤 레지스트리와 오브젝트 스코프

우리는 오브젝트 팩토리를 만든것과 애플리케이션 컨텍스트를 사용한것 2가지를 만들었습니다.

이 둘은 중요한 차이점이 존재합니다.

만약, DaoFactoryuserDao() 메서드를 2번 호출하면 2개의 오브젝트는 같을까? 라는것 입니다.

다음과 같은 코드로 오브젝트를 확인해봅시다.

// 오브젝트 팩토리 방식
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

System.out.println(dao1);
System.out.println(dao2);

확인해보면 출력값이 서로 다른것을 확인할 수 있습니다.

이를 통해서 userDao() 를 매번 호출하면 계속해서 새로운 오브젝트가 생성된다는 것을 알 수 있습니다.

하지만 만약, ApplicationContext 를 사용한 것에서는 어떤지 확인해봅시다.

// ApplicationContext 사용
ApplicationContext = context = new AnnotationConfigApplication(DaoFactory.class)

UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

System.out.println(dao3);
System.out.println(dao4);

만약 ApplicationContext 를 실행하면 출력값이 서로 같은 것을 볼 수 있습니다.

결론적으로 여러개의 오브젝트를 생성하려고해도 매번 동일한 오브젝트를 돌려준다는 것 입니다.


싱글톤 레지스트리로서의 애플리케이션 컨텍스트

애플리케이션 컨텍스트는 IoC 컨테이너로 역할을 하지만 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(Singleton Registry)입니다.

스프링에서는 별다른 설정을 하지 않는 이상 내부에서 생성하는 빈 오브텍트를 싱글톤으로 만듭니다.


서버 애플리케이션과 싱글톤

만약에 우리가 사용하는 UserDao 가 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트가 새로만들어진다고 가정한다면

우리는 계속해서 요청에 따라서 수만개의 오브젝트를 생성하고 지우고를 반복해야합니다.

이러면 서버에 많은 부하가 걸리게 됩니다. 그래서 우리는 싱글톤을 사용하게 됩니다.

싱글톤 패턴은 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것 입니다.

이러한 싱글톤 패턴은 장점만 있을것 같지만 안티패턴으로 피해야하는 패턴으로 불리기도 합니다.


싱글톤 패턴의 한계

싱글톤을 구현하기 위해서는 다음과 같은 방식으로 구현을 합니다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하게 생성자를 private로 만듭니다.
  • 생성도니 싱글톤 오브젝트를 저장하기 위해서 자기와 같은 타입의 static 필드를 정의합니다.
  • 스태틱 팩토리의 메서드인 getInstance()를 만들고 메서드가 오출되는 시점에서 한번의 오브젝트가 만들어지게 합니다.
  • 한번의 오브젝트가 만들어지고 난 뒤는 getInstance()를 통해서 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨줍니다.

이렇게 만든 UserDao의 예제를 살펴봅시다.

public class UserDao{
    private static UserDao INSTANCE;

    public static synchronized UserDao getInstacne(){
        if(INSTANCE == null) INSTANCE = new UserDao(???);
        return INSTANCE;
    }

    private UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
}

이런식으로 만들어진 싱글톤 패턴은 코드가 상당히 지저분해지고 private로 바귄 생성자는 외부에서 호출을 할 수 없기 때문에 DaoFactory 에서 UserDao 를 생성하며 ConnectionMaker 오브젝트를 넣어지는것이 더이상 안된다.

이러한 싱글톤 패턴구현 방식에는 다음과 같은 문제가 존재합니다.

  • private 생성자를 가져 상속할 수 없다.
    • private 를 사용해서 생성자를 제한한다면 객체지향의 장점인 상속과 이를 이용한 다형성을 사용할 수없다. static과 메서드를 사용하는 것도 객체지향의 장점을 사용할 수 없게 한다.
  • 싱글톤은 테스트하기가 힘들다
    • 싱글톤은은 만들어지는 방식이 제한적이여서 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들어서 테스트하기가 힘듭니다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 서버에서 클래스 로더를 어떻게 구성하고 있냐에 따라서 싱글톤 클래스여도 하나이상의 오브젝트가 만들어질 수 있습니다.
  • 싱글톤 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
    • 싱글톤은 static 메서드를 이용해서 애플리케이션 어디서든지 사용될 수 있습니다. 이러한 방식은 객체지향 프로그래밍에서 권장되지 않는 프로그램 모델입니다.

싱글톤 레지스트리

자바의 기본적인 싱글톤 패턴 구현 방식은 여러가지 단점이 존재합니다.

그래서 스프링은 싱글톤 레지스트리(Singleton Registry)를 사용해서 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다.

이러한 싱글톤 레지스트리는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너라고도 볼 수 있습니다.

기존에 사용했던 privatestatic을 사용해서 구현한 클래스가 아닌 기본 클래스에서 싱글톤으로 관리를 해줘서 단점을 보완합니다.

그래서 스프링이 지지하는 객체지향적인 설계 방식과 디자인 패턴등을 적용하는데 아무런 제약이 사라집니다.

이제 주의할점들을 살펴봅시다.


싱글톤 오브젝트와 상태

싱글톤은 멀티스레드 환경에서 여러 스레드가 동시에 접근해서 사용할 수 있습니다.

그렇기 때문에 더욱 상태 관리에 주의를 기울여야합니다.

싱글톤은 우선 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되면 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야합니다.

이런것들을 파라미터나 로컬 변수, 리턴 값등을 이용해서 상태정보를 다룹니다.

//인스턴스 변수를 사용하도록한 UserDao
public class UserDao{
    private ConnetcionMaker connectionMaker;
    private Connection c;
    private User user;

    public User get(String id) throws ClassNotFoundException, SQLException{
            this.c = connectionMaker.makeConnection();

            /...
            this.user = new User();
            this.user.setId(rs.getString("id"));
            this.user.setName(rs.getString("name"));
            this.user.setPassword(rs.getString("password"));

            /...
            return this.user;
    }
}

이 코드는 기존가 다른점으로는 원래는 ConnectionUser는 로컬 변수로 각 메서드에서 선언해서 사용했는데 이를 클래스의 인스턴스 필드로 선언했다는 것 입니다.

이런식으로 로컬이 아닌 인스턴스 변수로 정의해서 사용한다면 심각한 문제가 발생합니다.

A가 접근을 할때 B도 동시에 접근을한다면 A의 정보를 추가해야하는데 B의 정보를 잘 못 추가할 수도 있기 때문입니다.

하지만 현재 기존코드에서도 connectionMaker는 인스턴스 변수를 사용을 했습니다.

하지만, 이것은 문제를 일은키지 않습니다. 왜냐하면 읽기전용 정보이기 때문입니다.

또한, 이 변수는 @Bean을 붙여서 싱글톤으로 관리되어 있습니다.

이런식으로 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도일 경우는 인스턴스 변수를 사용해도 무관합니다.

또한, 읽기전용 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋습니다.

그렇지만 단순한 읽기전용이라면 static final 이나 final 을 사용하는 것이 더욱 효율적 입니다.


스프링 빈의 스코프

스프링이 관리하는 오브젝트인, 빈이 생성되고, 적용되는 범위는 10장에서 배우게 됩니다.

보통은 스프링 빈의 기본 스코프는 싱글톤입니다.

이러한 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서 강제로 제거하지 않는 이상 스프링 컨테이너가 존재하는 동안 계속 유지됩니다.

처음으로 프론트와 협업중에서 Contorller의 구현이 끝나서 확인하기 위해서 배포를 진행했습니다.

배포를 진행해서 확인을 하려고하는데 프론트에서 다음사진과 같은 문제 때문에 값이 안받아진다고 했습니다...

프론트에서 CORS 오류...

아니 도대체 CORS가 뭔데 오류인건지 확인을 해보니 저 뿐만이 아닌 모든 프론트&백엔드 개발자가 겪는 오류여서 무엇이 문제인지 CORS가 무엇인지 정리하기 위해서 글을 작성해봅니다.


CORS란

CORS(Cross-Origin Resource Sharing)은 언제발생하냐면 내 출처가 아닌 다른 출처에서 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에게 알려주는 체제라고합니다.

뭔 소리인지 자세하게 알아보겠습니다. 우선 내 출처가 아닌 다른 출처 라는 것을 확인해보겠습니다.

만약, http://chan-coding-book.com:8080 이라는 출처가 바로 내 출처라고 생각해봅시다.

uri를 구성하는 요소는 다양하게 존재합니다 포트번호, 도메인, 프로토콜 등이 존재합니다.

  • 포트번호 : 8080
  • 도메인 : chan-coding-book.com
  • 프로토콜 : http

가 다음과 같이 존재를 합니다.

내 출처는 바로 포트번호, 도메인, 프로토콜이 같은 경우를 말합니다.

우리는 SOP(Same-Origin Policy)라는 규칙때문에 같은 출처에서만 리소스를 공유할 수 있습니다.

그렇기 때문에 내 출처가 아닌 다른 출처에서 리소스를 공유할 때는 CORS가 발생합니다.

물론 동일 출처(내 출처)인 경우는 SOP 덕에 리소스를 공유를 할 수 있습니다.

왜 막아두냐하면 CSRF 공격 때문에 그렇습니다.

해커가 마음대로 API를 통해서 웹사이트로 개인 정보를 탈취할 수 있기 때문에 SOP가 존재하는 이유입니다.


문제 발생 이유

그렇기 때문에 문제가 발생한 이유는 현재 프론트는 localhost:3000을 사용하지만 우리는 전혀 다른 도메인과 프로토콜을 사용하고있습니다.

그래서 출처가 달라져서 CORS 문제가 발생했습니다.

이제 그래서 CORS는 어떻게 동작하는지 살펴보겠습니다.


CORS의 동작원리

CORS는 동작이 3가지로 나누어집니다. Preflight requestSimple request, Credentialed Request입니다.

현재 오류코드를 다시 본다면 Preflight request에서 오류가 나왔다고 합니다.

우선 Preflight request가 무엇인지 먼저 살펴보겠습니다.


Preflight Request

Preflight라는 말을 이해하면 이해하기가 쉬울것 같습니다.

Pre는 먼저라는 소리이고 flight는 날아간다는 소리이기 때문에 먼저 날려본다라고 해석하면 좋을것 같습니다.

그래서 확인을 해보자면 우리가 다른 도메인의 리소스로 OPTIONS 메서드를 사용해서 HTTP 요청을 보내서 실제 요청이 전송하기에 안전한지 확인하는 것입니다.

왜냐하면 Cross-origin 요청은 유저 데이터에 영향을 줄 수 있기 때문입니다.

우리가 오류가 생긴이유는 먼저 보내봤는데 오류가 나왔기 때문에 그렇습니다.

Preflight request가 요청된 예제를 한번 살펴봅시다.

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

Request를 하나하나 확인해 봅시다. 우선, OPTIONS 이라는 메서드는 서버에서 추가 정보를 판별하는 메서드라고 합니다

이 메서드는 2개의 다른 요청 헤더가 전송이 됩니다. 헤더는 다음과 같습니다.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

그렇기 때문에 해석을 한번 해봅시다.

  • Access-Control-Request-MethodPOST로 실제 요청을 전송한다는 뜻입니다.
  • Access-Control-Request-HeadersX-PINGOTHERContent-Type이 전송이 된다는 뜻입니다.

그래서 결론적으로는 요청을 전송할 때 POST 메서드와 요청 헤더를 X-PINGOTHER으로 받을 수 있다는 것을 의미합니다,

나머지 것들도 살펴보도록 하겠습니다. 이 값들을 이해해야 Spring에서 설정을 할 수 있습니다.

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

Access-Control-Allow 라는 뜻은 직독하면 접근이 허용된것이라고 생각하면 됩니다. 그래서 결론은 다음과 같습니다.

  • Originhttp://foo.example 만 허용한다고 보입니다.
  • MethodPOST, GET, OPTIONS 만 허용하는 것을 볼 수 있습니다.
  • HeadersX-PINGOTHER, Content-Type 만 허용하는 것을 볼 수 있습니다.
  • Max-Age는 최대 캐싱 시간입니다. 86400이므로 24시간을 뜻합니다. 이는 숫자가 클수록 우선순위가 높습니다.

우리는 이러한 preflight request에서 오류가 나왔기 때문에 이를 spring security의 설정을 통해서 해결해보도록 합시다.


해결방안

우선 이러한 문제를 해결하기 위해서 WebSecurityConfigureAdapter 를 상속받는 WebSecurityConfig 라는 클래스를 만들었습니다.

@Configuration // 설정파일 등록
@EnableWebSecurity // 웹보안 활성화
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //...
        http.cors();
    }
}

그리고 다음과 같이 cofigure(HttpSecurity http) 라는 메서드를 오버라이드 합니다. 또한, 설정에 대한 @Configuration@EnableWebSecurity에 대한 애너테이션은 꼭 붙여야합니다.

이후에 http.cors() 라는 메서드를 호출합니다.

하지만 이렇게 설정을해도 계속 오류는 발생합니다. 이러한 오류를 해결하기 위해서 Access-Control에 대한 범위를 설정을 해줍니다.

설정을 하기 위해서 WebMvcConfigure 를 구현하는 WebConfig라는 클래스를 다음과 같이 만들것 입니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry){
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(false);
    }
}

그 후 addCorsMappings(CorsRegistry registry) 메서드를 오버라이딩 합니다.

이후에 registry에 대해 허용을 설정을 하나씩 합니다.

  • addMapping 을 통해서 어떤 주소에 대해서 설정할지를 결정합니다. "/**"를 명시하면 모든 경로에 대해서 설정입니다.
  • allowedOrigins 를 통해서 허용할 Origin에 대해서 설정합니다.
  • allowedMethod 를 통해서 허용할 메서드를 설정합니다.
  • allowedHeders 를 통해서 허용할 Header들을 설정합니다.
  • allowCredentialsfalse로 설정해야 Origins*이라는 와일드 카드가 들어갈 수 있습니다.

다음과 같은 설정을 한다면 해결이 될 것입니다.


Reference

'Spring > 디버깅' 카테고리의 다른 글

Json이 LocalDateTime에서 잘못 넘겨지는 경우  (0) 2022.02.23

스프링 프로젝트를 만드는 도중에 LocalDateTime 이라는 항목에 대해서 Json으로 반환을 할때 다음 사진과 같이 넘겨지는 오류를 발견했습니다.

image


원래는 다음 그림과 같이 날짜와 시간등이 변환이 되어야합니다.

image

문제점을 알기위해서 확인해보니 다음과 같은 방식으로 해결할 수 있었습니다.

해결방법

기존 LocalDateTime 을 넘겨주는 DTO는 다음과 같은 인스턴스 변수로 생성되어 있습니다.

private LocalDateTime createAt;

이러한 코드에서 @JsonFormat을 통해서 패턴을 넣어서 String 형으로 다시 반환을 할 수 있게 할 수 있습니다.
코드는 다음과 같습니다.

@JsonFormat(shape= JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime createAt;

그러면 해결이 되는 것을 볼 수 있습니다.

'Spring > 디버깅' 카테고리의 다른 글

[Spring/오류수정] CORS(Cross-Origin Resource Sharing)  (0) 2022.02.25

오브젝트와 의존관계

DAO

DAO란 (Data Access Object)라고 하고 이것은 데이터를 조회하거나 조작하는 기능을 전담하게 하는 객체를 말합니다.

보통 DAO를 사용하기 위해서는 DTO라는 (Data Transfer Object)를 만들게 됩니다.

정말 간단하게 DTO는 사용자가 어떤 데이터를 받는지 객체에 저장하는 것을 말합니다.

이들은 Getter/Setter로 묶여있어 데이터를 접근하거나 반환받을 수 있습니다.

우선 User라는 DTO를 만들도록 하겠습니다.

public class User{
    String id;
    String name;
    String password;

    public String getId(){
        return id;
    }

    public void setId(String id){
        this.id = id;
    }

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getPassword(){
        return password;
    }

    public void setPassword(String password){
        this.password = password;
    }

}

이러한 DTO는 그냥 현재 id, name, password로 구현이 된 상태입니다.

이런식으로 Getter/Setter만 있는 것이 DTO입니다

이러한 DTO에 데이터 베이스에서 꺼낸 정보가 들어가게 됩니다.

이러한 DTO에 꺼내고 저장하는 역할을 하는것이 바로 DAO입니다.

일단 데이터 베이스를 구축하기 위해서 테이블을 하나 만듭니다

필드와 정보는 다음과 같습니다

User 테이블

다음처럼 테이블을 구성한다. 이때 처음 배운다는 가정하에 Primary Key를 모를 수도 있다고 생각합니다.

이러한 Primary Key는 고유의 값이라고 생각하면 편합니다.

Primary Key라고 설정을 하면 아무도 중복의 값을 가질 수 없습니다

이러한 테이블을 생성하기 위한 SQL 문법은 다음과 같습니다

create table users(
    id varchar(10) primary key,
    name varchar(20) not null,
    password varchar(20) not null
)

이렇게 입력을하고 실행을 하면 DB가 생성이 됩니다.

이제 UserDao를 생성해봅시다!!

이름은 당연히 UserDao라는 이름으로 클래스를 생성할 것 입니다.

또한, 우선적으로 다음과 같은 메서드를 생성할 것 입니다

  • 새로운 사용자를 생성하는 기능 (add 라는 메서드)
  • 아이디를 가지고 사용자의 정보를 가져오는 기능 (get 이라는 메서드)

이러한 기능을 JDBC를 이용해서 진행할 예정입니다

JDBC의 작업순서

JDBC를 작업하기 위해서는 다음과 같은 순서를 통해서 작업을 할 수 있습니다.

우리는 스프링을 배우고 있고 JDBC도 물론 배우겠지만 중점적으로 할 것은 아니기 때문에 일단 이런과정을 통해서 사용이 되는구나 라고 알아두면 좋을 것 같습니다

  • DB 연결을 하기 위해서 Connection이라는 객체를 만들어 연결한다
  • SQL을 담은 Statement를 만든다
  • 만들 Statement를 실행한다
  • 조회해서 SQL 쿼리의 실행결과들을 ResultSet으로 받아서 User라는 미리만든 DTO에 넣어준다.
  • 작업이 끝난 후는 이 때 생성한 Statement, ResultSet, Connection 들을 작업을 마친 후에 반드시 닫아주어야 합니다
  • JDBC API가 만들어내는 예외는 직접 처리하거나 메소드에 throws를 넘겨서 메소드 밖으로 던지게 합니다.

그래서 생성하면 다음과 같은 코드가 나옵니다

import java.sql.*;
//...//
public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql:// 내 쿼리주소","spring","book"
        );

        PreparedStatement ps = c.prepareStatement(
                "insert into users(id, name, password) values(?,?,?)"
        );
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public void get(String id) throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql:// 내 쿼리주소","spring","book"
        );

        PreparedStatement ps = c.prepareStatement(
                "select * from users where id = ?"
        );
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

여기서 Connection과 Class를 건너뛰고 설명하자면

  • PreparedStatement는 내가 입력하고자하는 SQL문이 들어가는 곳이다.
  • ps.setString(1,id)이런식의 메서드 활용이 많은데 이 뜻은 SQL문의 첫 번째 ?의 값을 id라고 설정하는 것이다.
  • ps.executeQuery()를 사용해서 쿼리를 실행해서 ResultSet에 저장한다.

그래서 이러한 코드를 짜게 되었습니다. 이제 테스트코드를 사용해서 테스트를 한번 해봅시다😊

테스트 코드는 main()메서드에 생성을해서 add()get()메서드를 검증 해보도록 합니다

public static void main(String[] args) throws ClassNotFoundException, SQLException{
    UserDao dao = new UserDao();

    User user = new User();
    user.setId("haecahn");
    user.setName("유해찬");
    user.setPassword("1234");

    dao.add(user);

    System.out.println(user.getId() + "제대로 나왔습니다");

    User user2 = dao.get(user.getId());
    System.out.println(user2.getName());
    System.out.println(user2.getPassword());

    System.out.println(user2.getId() + "제대로 나왔습니다");

}

이런식으로 확인하면 제대로 값이 나오는 것을 볼 수 있다.

하지만 이러한 코드는 스프링의 개발을 한 원칙으로 살펴보면 무척이나 무성의한 코드로 볼 수 있다.

이러한 것을 Spring 스타일로 개선하는 작업을 이 책에서는 진행하려고 합니다.

DAO의 분리

관심사의 분리

객체지향에서는 모든것이 변합니다. 비지니스에서 A를 개발하라고 했다가 다시 B로 돌아와라 라고 할 수도 있고 이러한 오브젝트에 대한 설계와 구현한 코드는 계속 게속 바뀝니다.

그래서 우리가 중점적으로 생각해야할 것은 미래를 위해서 다시 잘 사용하고 유지보수가 쉽게 설계하는 것을 목표로 하는 것이 개발자들이 가장 염두해야할 사항입니다.

이런 것이 너무 미래라고 생각할지도 모르지만 당장 코드를 짜고나서 한 시간 뒤에도 이런 변화는 찾아올 수 있기 때문에 객체지향 기술을 모두 잘 활용해서 이를 자유롭고 편리하게 변경, 발전, 확장시킬수 있어야합니다

이러한 것을 보다 효율적으로 하기 위해서 관심사의 분리라는 프로그래밍의 기초 개념을 이용합니다.

이러한 관심사의 분리라는 것은 관심사가 같은 것 끼리는 하나의 객체 안으로

하지만, 관심사가 다르다면 서로 영향을 주지 않기 위해서 분리하는 것 입니다.

물론 코드 작성은 그냥 관심사를 상관없이 대충 비슷한것 끼리 짜는 것이 편하지만

이를 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어주는 것이 좋습니다.

UserDao의 관심사항

우리가 아까전에 작성한 UserDao를 다시 돌아보면서 관심사항에 대해서 정리를 해봅시다.

총 3가지로 나뉠 수 있습니다

  1. DB와의 연결을 위해서 우리는 Connection을 연결하고 가져왔습니다. 이러한 것을 DB연결에 대한 관심이라고 하나로 정의할 수 있습니다.
  2. DB에 보낼 SQL Satement를 만들고 실행하는 것으로 볼 수 있습니다.
  3. 마지막으로 사용한 Statement, ReusltSet, Connection들을 닫아주는 일을 하는 것이 있습니다.

첫 번째로 DB연결에 대한 관심사를 살펴보겠습니다.

현재는 각 메소드마다 동일한 Connetion정보인데 이를 여러번 사용하는 것을 볼 수 있습니다.

우리는 앞으로 수천 개의 DAO코드를 만들수도 있는데 이러한 것이 계속해서 중복으로 있다면 충분히 성능에 영향을 끼칠 것이라고 생각합니다.

만약 진짜 Connection에 대한 정보가 바뀐다면 모든 DAO에 있는 수천개의 Connection의 코드를 바꿔주어야합니다. 이러한 코드를 방지하기 위해서 메소드로 추출할 것 입니다.

중복 코드의 메소드 추출

이러한 중복된 DB 연결 코드를 getConnection이라는 메서드를 만들어서 수정할 것입니다.

동일하게 getConnection이라는 메서드에는 연결하는 코드가 존재합니다.

이제 연결을 할때는 그저 하나의 메서드만 사용하면 됩니다.

private Connection getConnection() throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
            "jdbc:mysql:// 내 쿼리주소","spring","book"
    );

        return c;
} 

이제 DAO에 대한 메서드가 엄청 많아진다고 해도 Connection을 할 때 접속 Address가 바뀌거나 하는 일이 생기면 getConnection 코드만 고치면 됩니다.

이러한 코드는 다른 코드에 영향을 주지도 않고 관심 내용이 독립적이기 때문에 수정도 간단해집니다.

변경 사항에 대한 검증

이러한 코드가 당연하게도 이전 코드를 똑같이 가져다 썼기 때문에 괜찮다 라고 생각할 수 있지만

항상 검증은 필요하다. 어떠한 문제점이 발견될 수 도 있기 때문이다.

DB 커넥션 만들기의 독립

현재 메서드를 분리해서 Connection을 할 수 있도록 만들었습니다.

하지만 이렇게 만든 기술을 A회사와 B회사가 구매를 주문해서 사용한다고 합니다.

그런데 A회사랑 B회사가 각기 다른종류의 DB를 사용하고있고 DB 커넥션을 가져오는데 독자적인 방법을 사용한다고 합니다

또한, DB커넥션을 UserDao라는 우리가 만들 프로그램을 구입하고 나서도 계속 변경될 가능성이 있다고 합니다.

이러한 상황에서는 우리가 UserDao에 대한 소스코드를 공개하고나서 getConnection을 수정해서 사용하라고 하면 편하지만

이러한 UserDao는 우리만의 독자적인 기술이고 이러한 코드를 공개하고 싶지 않다 라고 했을 경우는 어떻게 해야할까요?

정말 난감한 상황입니다. 공개를 하고 싶다고 해서 소스코드를 줘버리면 다른 회사가 우리의 기술을 모두 익혀버려서 주문이 안들어올 수 도 있고 이러한 과정에서는 어떠한 방법을 사용해야할까요?

상속을 통한 확장

이런경우는 우리가 사용하고 있는 UserDao 의 코드를 한 단계 더 분리하면 해결할 수 있습니다.

getConnection을 추상 메서들로 만들어 버리면 A라는 회사와 B라는 회사에서 알아서 필요한 부분을 구현할 수 있기 때문에 나머지 UserDao라는 클래스의 코드는 그냥 getConneciton 메서드를 따로 수정할 필요도 없고 코드를 수정할 필요도 없어집니다.

우리가 생성한 UserDao라는 추상 클래스를 통해서 A회사와 B회사에 판매를 하면 이제 각각 회사에서 자식클래스를 만들어서 getConnection을 원하는 형태로 만들 수 있습니다.

그렇게 된다면 자유롭게 회사 입맛대로 getConnection이라는 메서드를 자유롭게 구현할 수 있습니다.

또한, 우리 회사는 UserDao에 대한 구현 기능을 넘기지 않아도 가능합니다.

// UserDao에서 추상 메서드로 만든 getConnection
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;

// A회사의 새로만든 클래스에서 구현한 getConnection()
public class AUserDao extends UserDao { // UserDao를 상속받음
    public Connection getConnection() throws ClassNotFoundException,
                SQLException{
                // A회사의 코드
            }
}

// B회사의 새로만든 클래스에서 구현한 getConnection()
public class BUserDao extends UserDao { // UserDao를 상속받음
    public Connection getConnection() throws ClassNotFoundException,
                SQLException{
                // B회사의 코드
            }
}

잘 살펴보면 상속을 받아서 추상 메서드인 getConnection을 구현함으로서 클레스 레벨로 AUserDaoBUserDao가 독립적으로 분리되는 것을 볼 수 있습니다.

이제 UserDao의 코드를 건들일 필요 없이 각 회사의 클래스를 만들어서 거기서 getConnection이라는 추성 메서드만 생성하면 됩니다.

이런식으로 UserDao처럼 슈퍼 클래스에 기본적인 로직의 흐름을 만들고

그 기능의 일부를 추상 메서드, 오버라이딩이 가능한 protected메서드로 만들어서

서브 클래스에서 메서드를 필요에 맞게 구현하는 방법을 템플릿 메서드 패턴이라고 합니다.

이러한 템플릿 메서드 패턴이라는 디자인 패턴은 스프링에서 자주사용하는 패턴입니다

다른 관점에서 보면 UserDao에서의 getConnection이라는 메서드는 Connection타입의 객체를 생성한다는 기능을 정의 해놓은 추상 메서드라고 할 수 있습니다.

그래서 UserDao의 서브 클래스의 getConnection 메서드는 어떠한 Connection 클래스의 객체를 어떻게 생성할 것인지 결정하는 방법이라고도 볼 수 있습니다.

이러한 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을

팩토리 메서드 패턴이라고도 부릅니다.

이렇게 변화한 코드도 장점만 있는 것은 아닙니다. 이제 슈퍼클래스를 변화한다면 서브클래스를 수정하거나 다시 개발해야 할 수도 있습니다.

또한, 확장된 기능인 DB Connection을 생성하는 코드는 오직 UserDao에서 만 사용 가능하고 다른 DAO 클래스에서는 사용할 수 없다는 단점이 생깁니다.

DAO의 확장

모든 오브젝트는 변합니다. 하지만 반드시 오브젝트가 다 동일한 방식으로 변하는 것은 아닙니다.

변화의 성격은 여러가지 특징에 따라 달라집니다. ex) JDBC API를 사용하는가?, JPA를 사용하는가?

현재까지는 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꾸어 쓸수 있도록 변화시켰습니다.

하지만, 여러가지 단점도 많은 것을 알게 되었습니다. 상속이라는 방법은 좋았지만 그럼에도 불구하고 단점이 존재합니다.

클래스의 분리

그래서 다른 방식으로 클래스를 분리하면서 두 개으 관심사를 독립시키고 동시에 손쉽게 확장할 수 있는 방법을 알아 봅시다.

현재까지는 다음과 같이 분리를 진행하였습니다.

  1. 독립된 메소드를 사용해서 분리했습니다. → getConnection을 만들기
  2. 상 하위 클래스로 만들어서 분리 했습니다. → 추상 메서드 사용

이제는 상속관계도 아닌 완전히 독립적인 클래스로 만들어보려고 합니다.

DB 커넥션과 관련된 부분을 서브클래스로 하는 것이 아닌 새로운 클래스를 만들어서

UserDao가 이것을 이용하게 만들면 됩니다.

우리는 SimpleConnectionMaker라는 클래스를 만들고 getConnection 기능을 그안에 넣을 것 입니다.

그러면 UserDao 에서는 SimpleConnectionMakernew를 통해서 객체를 만들어서 각각의 메서드에서 사용할 수 있게 합니다.

이 말은 꼭 add()get()에서 SimpleConnectionMakernew를 통해 객체를 만들라는 소리가 아니라

한번만 SimpleConnectionMaker를 만ㄷ르어서 저장해두고 사용할 것 입니다.

public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker; // 한 번만 생성자를 통해서 만든다.

    public UserDao(){
        simpleConnectionMaker = new SimpleConnectionMaker(); // 객체 생성 -> 생성자라 한번만 생성
    }
}

이렇게 생성자를 통해서 하나만 만들면 다른 메서드에서 어떻게 사용하는지 확인해보자.

public void add(User user) throws ClassNotFoundException, SQLException{
        Connection c = simpleConnectionMaker.makeNewConnection();
}

public void get(String id) throws ClassNotFoundException, SQLException{
        Connection c = simpleConnectionMake.makeNewConnection();
}

그리고 이제 DB 커넥션 생성 기능을 독립시킨 SimpleConnectionMaker는 다음과 같이 만들 수 있다.

makeNewConnection()이라는 메서드를 통해서 Connect를 해준다. 이제 상속을 사용하지 않으니 더이상 getConnection() 기능을 하는 makeNewConnection()abstract가 아니여도 된다.

public class SimpleConnectionMaker{
        public Connection makeNewConnection() throws ClassNotFoundException,
        SQLException{
                Class.forName("com.mysql.jdbc.Driver");
                Connection c = DriverManager.getConnection(
                        "SQL 주소"
                )
                return c;
        }
}

이를 통해서 Connection을 반환해준다.

이렇게 해서 분리는 하였지만 다시 문제가 발생하였습니다

이제 UserDao의 코드는 SimpleConnectionMaker라는 특정 클래스에 종속이 되기 때문입니다.

그렇기 때문에 상속을 사용했을 때 처럼 UserDao의 코드 수정없이 A회사랑 B회사가 Connection을 생성하는 기능을 변경할 방법이 사라졌습니다.

우리가 해결하야하는 문제는 그래서 다음과 같습니다.

  1. SimpleConnectionMaker 의 메서드의 문제
  2. 현재 makeNewConnection()으로 DB 커넥션을 가져오게 만들었습니다 이것으로 각 메서드에 Connection을 할당해줍니다. 만약, 변경이 있으면 add(), get() 뿐만아니라 많은 메서드가 생성이 되었으면 이를 일일히 변경하는 것은 무척이나 비효율적인 문제입니다.
  3. DB 커넥션을 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고 있어야한다는 것입니다.왜냐하면 SimpleConnetionMaker라는 클래스 타입의 인스턴스 변수까지 정의를 해놨기 때문에 UserDao자체를 다시 수정해야합니다.그래서 다른회사가 현재 우리의 UserDao 기술을 구매하려고하면 자유롭게 확장하기 때문에 상속을 안쓴만 못합니다.
  4. 현재 근본적인 원인으로는 UserDao가 DB 커넥션을 가져오는 클래스에 대해서 너무 많이 알고 있기 때문에 커넥션을 가져올 때마다 우리는 메서드가 이름이 뭐고 어떤 클래스가 사용되는지 모든것이 구체적인 방법에 종속됩니다.
  5. 만약 다른회사에서 다른 클래스를 구현하려고 한다면 불가피하게 UserDao를 수정을 해야 합니다.

인터페이스의 도입

이를 해결하기 위해서는 현재 UserDaoSimpleConnectionMaker가 너무 긴밀하게 연결되어 있기 때문에 그렇습니다.

이러한 둘이 긴밀하게 연결을 끊기 위해서 중간에 추상적인 느슨한 연결고리를 만들어주는 것이 좋습니다.

이러한 것을 인터페이스를 사용해서 공통적인 성격을 뽑아 내서 이를 따로 분리해내면 해결할 수 있을 것 같습니다.

우리가 인터페이스를 이용해서 접근하게 한다면 만약 SimpleConnecitonMaker가 아닌 HaechanConnectionMaker를 바꿔서 사용해도 신경쓸일이 없습니다.

왜냐하면 인터페이스는 변하지 않기 때문입니다.

이제 우리는 다음과 같이 구현할 것입니다.

UserDao에서는 ConnectionMaker라는 인터페이스를 사용합니다

ConnectionMakermakeConnection()이라는 메서드를 가지고

이러한 makeConnection()이라는 메서드는 각 회사의 사정에 맞춰서 구현된 클래스에 의해서 직접 구현하면 됩니다.

이제 ConnectionMaker 인터페이스를 정의해 봅시다. 이때 DB 커넥션을 가져오는 메서드 이름은 makeConnection()이라고 정의 할 것입니다.

public interface ConnectionMaker{
        public Connection makeConnection() throws ClassNotFoundException,
        SQLException;
}

이제 납품을 할때 다음과 같이 Interface를 사용해서 이제 UserDao와 함께 ConnectionMaker라는 인터페이스를 전달하면 각 회사에 맞게 인터페이스를 구현한 클래스를 만들고 makeConnection()메서드를 작성하면 됩니다.

그러면 인터페이스를 사용한 UserDao는 어떻게 변했냐면 다음과 같습니다.

public class UserDao{
    private ConnecitonMaker connectionMaker;

    public UserDao(){
        connectionMaker = new AConnectionMaker();
    }

    public void add(User user) throws ClasNotFoundException, SQLException{
        Connection c = connectionMaker.makeConnection();
    }

    public void get(String id) throws ClassNotFoundException, SQLException{
        Connection c = connectionMaker.makerConnection();
    }
}

변하긴 했는데 여기서도 connectionMaker = new AConnectionMaker()에 대해서는

아직도 AConnectionMaker()의 클래스의 생성자를 호출해서 객체를 생성하는 코드가 남아있다.

다른 곳에서는 이러한 정보들을 모두 지웠는데 결론적으로는 AconnectionMaker()라는 클래스 이름을 넣어서 객체를 만들지 않으면 사용을 할 수가 없다.

결론적으로 모두 지우지 못해서 원점으로 돌아오게 되었다.

관계설정 책임의 분리

현재까지의 진행상황은 UserDaoConnectionMaker라는 두 개의 관심을 인터페이스를 쓰면서 분리를 완료했습니다

하지만, 어찌해도 UserDao는 어떤 Connection을 사용할지 생성자에 명시가 되어있는 것을 볼 수 있습니다.

결론적으로는 Connection을 다른 회사에서 사용하면 UserDao는 아직까지도 무조건 변경을 해야합니다.

이렇게 된 이유는 UserDao안에 우리가 아직 분리하지 않은, 또 다른 관심사항이 존재하기 때문입니다.

현재 UserDao안에는 어떤 ConnectionMaker구현 클래스를 사용할지 결정하는 new AConnectionMaker()라는 코드가 존재합니다.

이 새로운 객체를 생성하는 코드는 매우 짧고 간단하지만 그 자체로 충분히 독립적인 관심사를 담고 있습니다.

왜냐면 이러 코드는 현재 UserDao의 관심사인 JDBC API와 User 객체를 이용해서 DB에 정보를 넣고 빼는 것을 하는 것도 아니고

ConnectionMaker인터페이스로 대표되는 DB 커넥션을 어떻게 가져올 것인가라는 관심사도 아니기 때문입니다.

결론적으로는 UserDaoUserDao 가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 대한 관심을 가지고 있다

UserDao가 지닌 관심사와 new AconnectionMaker()라는 객체를 생성하는 관심사가 충돌하고 있기 때문에

결코 독립적으로 확장 가능한 클래스가 될 수 없습니다.

UserDao를 현재는 사용하는 클라이언트는 적어도 하나 존재합니다.

이러한 UserDao의 오브젝트의 기능을 사용하는 클라이언트가 UserDao를 사용하기 전에 ConnectionMaker의 구현 클래스를 사용할지 결정을 하도록 만들어 봅시다.

결론적으로 지금 우리가 하는일은 UserDao 오브제긑와 특정 클래스로 만들어진 ConnectionMaker 오브젝트 사이에 관계를 설정해주려고 하는 것 입니다.

현재 우리가 사용하고 있는 코드인 connectionMaker = new AConnectionMaker()

AconnectionMaker의 오브젝트의 Reference를 UserDao가 가지고 있는 connectionMaker라는 변수에 넣어서 사용하게 함으로써 사용이라는 서로의 관계를 맺어줍니다.

이런식으로 객체 사이의 관계가 만들어지기 위해서는 만들어진 객체가 있어야합니다.

지금까지 우리는 생성자를 통해서 객체를 만들어 왔습니다.

하지만 꼭 생성자를 사용하지 않고 외부에서 만들어준 것을 가져오는 방법도 존재합니다.

우리는 현재까지 생성자를 통해서 UserDao의 코드 내에서 객체를 생성해주었습니다.

이제 외부에서 만든 객체를 Parameter를 통해 가져온다면 사용을 할 수 있습니다.

이런 방식으로 한번 다시 리팩토링을 해봅시다.

현재는 connectionMaker = new AConnectionMaker()를 사용하고 있기 때문에 AConnectionMaker

UserDao가 불필요하게 알고 있습니다. 이러한 UserDao코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가지고 있으면 안됩니다!!

클라이언트는 현재 UserDao를 자기가 사용해야 할 입장이기 때문에 UserDao의 세부 전략인

ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 객체를 생성하여 UserDao와 연결할 수 있습니다.

이러한 관심을 분리해서 클라이언트에게 떠넘기는 방식으로 Parameter를 통해 사용해 봅시다.

UserDao 객체가 사용할 ConnectionMaker를 파라미터를 통해 전달받을 수 있는 형식으로 바꾸면 될 것 같습니다.

public UserDao(ConnectionMaker connectionMaker){
    this.connectionMaker = connectionMaker;
}

이런식으로 하면 클라이언트에게 책임을 넘겼기 때문에 AConnectionMaker가 사라진 것을 볼 수 있습니다.

이를 이제 넘겨주는 UserTest라는 클래스를 하나 만들어서 이러한 UserTest에서 원래 실행했던 new AConnectionMaker()를 생성하도록 합니다.

그리고 UserTest안에서 UserDao객체를 파라미터를 넘겨 생성하도록 합니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
        SQLException{
            ConnectionMaker connectionMaker = new AConnectionMaker(); // 객체 생성

            //Userdao 객체를 생성해서 내가 사용할 ConnectionMaker 객체를 넘겨준다.
            UserDao dao = new UserDao(connectionMaker); 
    }
}

이제 이렇게 생성된 UserDaoTestUserDaoConnectionMkaer 구현 클래스와의 런타임 객체 의존 관계를 설정하는 책임을 담당해아합니다.

이런식으로 까지 변환이 완료되었다면 테스트를 해봅시다!!

이제 UserDao에 있으면 안 되는 다른 관심사항을 클라이억트로 책임일 떠넘기는 작업을 완료했습니다.

UserDaoTest 덕분에 A와 B회사는 자신들이 원하는 DB 접속 클래스를 만들어서 UserDao가 사용할 수 있도록 할 수 있습니다.

그저 각자의 회사에서 만든 객체를 넘겨주기만하면 됩니다.

이렇게 만든 코드에서 DB 커넥션을 가져오는 방법을 어떻게든 변경해도 이제 UserDao에 영향을 주지는 않습니다.

이제 DB 접속 방법에 대한 관심은 한 군데에 집중되어 DAO가 많아져도 DB접속 방법이 변경되었을 때 한 곳만 고쳐주어서 유지보수하기 편하게 되었습니다.

원칙과 패턴

지금까지 DAO 코드를 수정하면서 객체지향 기술의 여러가지 이론을 사용했습니다.

이제 다시한번 우리가 사용한 객체지향 기술에 대해서 어떤 이론을 사용했는지 보도록 합시다.

개방 폐쇄 원칙(OCP)

개방 폐쇄 원칙(Open-Closed Priciple)을 이용한다면 지금까지 해온 리팩토링 작업의 특징과 최종적으로 개선된 설계와 코드의 장점이 무엇인지 효과적으로 설명이 가능합니다.

이 원칙은 간단히 설명하면 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야한다.라는 것을 의미합니다.

우리가 작성한 UserDao는 DB 연결 방법이라는 기능을 확장하는데는 매우 열려있습니다.

또한, UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있게 되어 있습니다.

그리고 UserDao 는 자신의 핵심 기능을 구현한 코드는 변화에 영향을 받지 않고 유지가 가능하기 때문에 변경에는 닫혀 있다고 말할 수 있습니다.

제일 처음 작성했던 코드를 다시 살펴보자. 우리는 DB Connection에 대해서 변경을 하려고 한다면

DAO 내부의 모든 메서드에 대해서 수정을 해야 했었습니다.

결론적으로 DB Connection을 확장하려면, DAO 내부도 변경해야 하기 때문에 개방 폐쇄 원칙을 잘 따르지 못한 설계라고 할 수 있습니다.

높은 응집도와 낮은 결합도

이러한 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도 라는 소프트웨어 개발의 원리로 설명이 가능합니다.

  • 높은 응집도
    • 응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻입니다.
  • 낮은 결합도
    • 하나의 변경이 발생할 때 어려가지 모듈과 객체에 대해 변경 요구가 전파되지 않는 상태를 말합니다.

높은 응집도는 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명이 가능합니다.

우리가 제일 처음 작성한 DAO에 대해서는 DB 커넥션을 바꾸면 어떤 일을 해야하냐면

  1. 변경이 필요한 부분을 모든 코드를 뒤져서 찾는다.
  2. 변경한 것이 DAO의 다른 기능에 영향을 주어서 오류를 발생시키지 않는지 확인해야한다.

하지만 우리가 만약 인터페이스로 DB연결 기능을 독립 시켰다면

  1. CoonectionMaker 구현 클래스를 새로만든다.

이게 끝이다. 시간이 대폭 단축될 수 있습니다. 인터페이스로 구현을 한다면 DAO에 대해서 변화하는게 없기 때문에 모든 DAO를 일일이 테스트를 하지 않아도 됩니다.

낮은 결합도는 높은 응집도 보다 더 민감한 원칙입니다.

책임과 관심사가 다른 객체 또는 모듈과는 느슨하게 연결된 형태를 유지하는 것이 바람직 하다는 것입니다.

현재 우리가 작성한 코드는 인터페이스를 통해서 느슨하게 연결되게 하였습니다.

이렇게 변하면 독립적이기 때문에 결합도가 낮아지면 변화에 대응하는 속도가 높아지고 구성이 깔끔해집니다.

또한, 확장하기에도 매우 편리합니다.

전략 패턴

우리가 최종적으로 만든 UserDaoTest → UserDao → ConnectionMaker 구조는 디자인 패턴의 시각으로 보면 전략 패턴에 해당한다고 볼 수 있습니다.

전략 패턴이란 무엇이냐면

  • 자신의 기능 맥락(Context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스로 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴을 말합니다

우리가 만든 DAO를 보면 UserDao 는 전략 패턴의 맥락(Context)에 해당됩니다.

UserDao가 기능을 수행하는 데 필요한 기능 중에서 변경 가능한 DB Connection이라는 알고리즘은

ConnectionMaker라는 인터페이스에 정의를 했습니다.

우리는 전략이 바뀔 때 즉, DB Connection을 하는 방식이 바뀔 때 ConnectionMaker를 변경만 하면 됩니다. 이래서 전략 패턴이라고 불립니다.

이러한 전략 패턴UserDaoTest와 같은 클라이언트의 필요성에 대해서도 잘 설명하고 있습니다.

전략 패턴은 Context(UserDao)를 사용하는 클라이언트(UserDaoTest)가 사용하는 전략(ConnectionMaker를 구현하는 클래스)를 Context의 생성자를 통해서 제공해주는 것이 일반적입니다.

Reference

  • 토비의 스프링 3.1 Vol. 1: 스프링의 이해와 원리

프로세스와 쓰레드

프로세스란 실행중인 프로그램이라고 이해하면 쉽다.

이를 실행하면 OS(운영체제)로 부터 실행에 필요한 자원을 할당받아 프로세스가 된다.

이러한 프로세스쓰레드로 구성이 되어있다.

쓰레드란 프로세서의 자원을 이용해서 실제로 작업을 수행하는 것이다.

으러한 쓰레드가 둘 이상으로 늘어나면 이를 멀티쓰레드 프로세스라고 한다.

멀티태스킹과 멀티쓰레딩

멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.

정말 쉬운 예제가 있다.

image

이런 코어가 사람이라고 생각하고 쓰레드가 손이라고 생각한다면

코어는 단 하나의 작업만 할 수 있다. 왜냐하면 사람 혼자니까

하지만 손을 여러개를 사용하여 금방 문제를 끝낼 수 있다. 라고 생각하면 된다.

그러면 쓰레드가많아지면 그만큼 성능이 좋다? 라고는 꼭 말할 수 는없다.

멀티쓰레딩의 장단점

멀티쓰레딩의 장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

예를들어 멀티쓰레딩을 못한다면 다음과 같은 상황이 벌어진다.

내가 유튜브를 보면서 친구와 카톡을 하지 못하는 것이다. 오직 한가지만 사용을 해야한다.

그래서 이런 서버 프로그램들은 서버 프로세스를 여러가지 생성하는 것 보다 쓰레드를 생성을 하는 것이 좀더 시간 적인 측면과 메모리적인 측면에서 이득을 볼 수 있다.

이러면 멀티쓰레딩만 쓰면 무조건 좋겠네? 라고 생각하는데 그렇지만은 않다.

멀티쓰레딩을 사용하기 위해서는 synchronization, deadlock 같은 문제점이 발생하기 때문에 신중히 사용해야한다.

쓰레드의 구현과 실행

쓰레드를 사용하기 위해서는 2가지 방법이 있다.

  1. Thread클래스를 상속받는 법
  2. Runnable인터페이스를 구현하는 방법

보통은 자바에서는 단일상속을 하기 때문에 만약 Thread클래스를 상속받아버리면

다른 클래스들을 상속받을 기회가 사라지기 때문에 2번의 방법을 선호한다.

Runnable()인터페이스는 다음과 같이 run()만 정의 되어있는 간단한 인터페이스이다.

class MyThread implements Runnable {
    public void run() { /... }
}

이러한 Thread클래스를 상속받아 사용할 때와 Runnable인터페이스를 구현한 경우의 인스턴스 생성방법은 다르다.

// Thread의 인스턴스 생성방법 (1)
ThreadEx t1 = new TreadEx();

// Runnable 인스턴스 생성방법 (1)
Runnable r = new ThreadEx();
Thread t2 = new Thread(r);

// Runnable 인스턴스 생성방법 (2)
Thread t2 = new Thread(new ThreadEx()); // 2줄이아닌 한번에 생성하는 법

또한, 둘의 차이점은 currentThread()에도 있다. 이는 현재 실행중인 쓰레드의 참조를 반환한느 메서드이다.

만약 Thread클래스에서 상속받았다면 부모인 Thread클래스의 메서드를 직접 호출이 가능하지만

Runnable인터페이스에서 상속받았다면 사용하기 위해서 Thread클래스의 static메서드인 currentThread()를 호출하는 방법으로 사용할 수 있다.

다음은 이에대한 예시이다.

// Thread클래스를 상속받았을 때
class ThreadEx extends Thread{
    @Override
    public void run() {
        System.out.println(getName());
    }
}

//Runnalbe 인터페이스를 사용할 때
class ThreadRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

다음 같이 Runnable인터페이스를 사용한 run()메서드는 Thread클래스를 사용해 static함수를 사용하여 불러오는 것을 볼 수 있다.

쓰레드의 실행

쓰레드를 생성해도 start()메서드를 호출하지 않으면 실행이 되지않는다.

그렇다고 start()메서드를 사용했다고 바로 실행되는 것이 아니라

실행 대기 상태에 있다가 자신의 차례일 때 실행이 된다.

이러한 실행순서는 OS에서 작성한 스케쥴에 의해 실행이 된다.

또한, 한 번 실행이 종료된 쓰레드는 다시실행 할 수없다는 점이 특징이다.

그래서 만약 start()메서드를 한 쓰레드에 대해 2번실행 한다면 IllegalThreadStateException이 발생하게 된다.

start()와 run()

이러한 start()run()메서드의 의미는 비슷한것처럼 느껴지지만 다르다.

일단 run()은 생성된 쓰레드를 실행시키는 것이 아닌 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

그렇지만 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해 생성된 호출에 run()이 첫 번째로 올라가게 한다.

이러한 호출스택은 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.

정리하자면 다음과 같이 실행이 된다.

  1. main메서드에서 쓰레드의 start()를 호출한다
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
  4. 호출스택은 이제 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

우리가 사용하는 main메서드도 작업을 수행하는 쓰레이드이다. 그래서 프로그램을 실행하면 우리도 모르게 하나의 쓰레드를 생성하여 main메서드를 수행한다.

싱글쓰레드와 멀티쓰레드

싱글쓰레드는 2개의 작업을 실행할 때 작업이 한 작업을 끝마치고 나서야 다른 작업을 처리할 수 있다.

하지만 멀티쓰레드는 쓰레드가 번갈아가면서 작업을 한다. 이 작업이 매우 짧은 시간이라 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.

하지만 이런것이 멀티쓰레드가 꼭 빠르다고 볼 수는 없다. 왜냐하면 번갈아가면서 작업을 하기 때문에 쓰레드간 작업 전환에 시간이 더 걸리기 때문에 더 많은 시간이 소요된다.

또는, 한 쓰레드가 작업을 하고 있는 동안 다른 쓰레드는 작업이 끝나기를 기다려야하는데 이때 발생하는 대기시간 때문에 그렇다.

결론적으로는, 하나의 자원을 사용하는 작업에 대해서는 싱글쓰레드가 효율적이다. 라고 말할 수 있다.

이러한, 멀티쓰레드로 진행한 작업은 실행할 때마다 다른 결과를 얻을 수 있다.

이유는 바로 프로세스가 OS의 프로세스 스케줄러의 영향을 받기 때문이다.

하지만 여러 개의 작업을 사용하는 작업일 떄는 멀티 쓰레드가 더 효율 적이다.

예를들어 메세지를 사용한다고 해보자

여기서의 작업은 2가지가 있다.

  1. 사용자의 입력을 기다리는 것
  2. 사람들의 카톡을 받는것

이거는 그냥 예시인것으로 기억하자!! 실제로는 아니다.

싱글쓰레드인 경우는 사용자가 입력을 할때 입력하는 작업에 집중해야 하기 때문에 다른사람의 카톡을 읽어오는 작업을 할 수 가 없다.

왜냐하면 싱글쓰레드는 한가지의 작업이 끝난 뒤 다음 작업을 할 수 있기 때문이다.

하지만, 만약에 멀티쓰레드라면 사용자의 입력을 하는 도중 다른 쓰레드가 입력을 기다릴 때

다른 사람의 메시지도 가져오는 것이다.

결론적으로, 여러개의 자원을 사용하는 작업에 대해서는 멀티쓰레드가 효율적이다.

다음 예시를 보자, 다음 예시는 2가지의 작업이 있다.

  1. 사용자의 입력을 받는 작업
  2. 10~1까지 출력하는 작업이 있다.

씽글쓰레드라면 다음과 같이 작동한다.

싱글쓰레드

//싱글 쓰레드 일때
public class temp {
    public static void main(String[] args) {

        String input = JOptionPane.showInputDialog("값을 입력해 주세요.");
        System.out.println("입력한 값은" + input + "입니다.");

        for(int i = 10 ; i > 0 ; i--){
            System.out.println(i);
            try{
                Thread.sleep(1000);
            }catch (Exception e) {}
        }

    }
}

멀티쓰레드 일때는 다음과 같다.

//멀티 쓰레드 일 때
public class temp {
    public static void main(String[] args) {

       ThreadEx th1 = new ThreadEx();
       th1.start();

       String input = JOptionPane.showInputDialog("값을 입력해 주세요");
        System.out.println("입력한 값은" + input + "입니다.");

    }
}

class ThreadEx extends Thread{
    @Override
    public void run() {
        for(int i = 10 ; i > 0 ; i--){
            System.out.println(i);
            try{
                sleep(1000);
            } catch (Exception e) {}
        }
    }
}

멀티쓰레드

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 속성을 가지고 있다.

이러한 우선순위의 순서에 따라서 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수도 있고

그렇기 때문에 사용자에게 빠르게 반응해야하는 특정작업들의 우선순위를 높여야 한다.

쓰레드의 우선순위 지정하기

쓰레드의 우선순위를 변경하는 메서드들은 다음과 같다.

void setPriority(int newPriority) // 쓰레드의 우선순위를 변경하는 메서드
int getPriority() // 쓰레드의 우선순위를 반환하는 메서드

// 전역변수
public static final int MAX_PRIORITY = 10 // 최대 우선순위
public static final int MIN_PRIORITY = 1 // 최소 우선순위
public static final int NORM_PRIORITY = 5 // 보통 우선순위

쓰레드의 우선순위는 특징이 있다.

  • 우선순위는 1~10이고 이는 숫자가 높을수록 우선순위가 높다
  • 쓰레드의 우선순위는 쓰레드를 실행하기 전에만 변경이 가능하다.
  • 멀티코어에서는 쓰레드의 우선순위에 따른 차이가 거의 없다.
    • 멀티코어에서는 쓰레드에 높은 우선순위를 주면 더 많은 실행시간과 실행기회를 갖게 될 것이라고 기대할 수 없다.
    • 빨리 끝나도 나머지 작업을 끝내야하기 때문에 그렇다. 시간이 비슷하다.
  • 어떤 OS에서 실행하느냐에 따라 스케줄러가 다르기 때문에 다른 결과를 얻을 수 있다.

쓰레드 그룹

쓰레드 그룹이란 서로 관련된 쓰레드를 그룹으로 다루기 위한 것이다.

이러한 쓰레드 그룹은 보안상의 이유로 도입된 개념이다.

쓰레드 그룹에 포합시키기 위해서는 Thread의 생성자를 이용해야한다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.

그렇기 때문에 만약 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는

기본적으로 자신을 생성한 쓰레드와 같은 그룹에 속하게 된다.

자바어플리케이션이 실행될때 JVM은 쓰레드 그룹을 생성한다.

JVMmainsystem이라는 쓰레드 그룹을 생성한다

이후 필요한 쓰레들을 생성해서 mainsystem쓰레드 그룹에 분류한다.

만약, 쓰레드그룹을 지정하지 않는다면 보통은 main쓰레드 그룹에 속하게 된다.

이러한 그룹에 관한 메서드는 다음과 같다.

ThreadGroup getThreadGroup() // 자신이 속한 쓰레드 그룹을 반환한다.
void uncaughtException(Thread t, Throwble e) // 쓰레드 그룹의 쓰레드가 처리되지 않은
//예외에 의해 실행이종료될 때, JVM에 의해 자동적으로 이 메서드가 호출된다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

지네릭스

지네릭스란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

예를 들어 ArrayList에서 만약 형을 정해주지 않으면 뜻하지 않은 형이 나타날 수도 있다.

나는 Integer만 저장하고 싶은데 String이 저장될 수 있고 이러한 것을 방지하고 또한, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄이기 위해 사용한다.

즉, 다시 말하자면 지네릭스의 장점은 이거라 할 수 있다.

  • 타입 안정성을 제공한다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

class Box<T>{
    T item;

    void setItem(T item) {this.item = item;}
    T getItem() {return item;}
}

이와 같이 본다면 C++에서 Template과 비슷한 점을 볼 수 있다.

또한, 이렇게 선언한 T를 타입변수라고 한다. 꼭 T만 사용할 수 있는것이 아니라 필요에 따라서 다른 문자를 사용해도된다.

이렇게 선언을 했다면 객체를 생성할 때 <T>를 붙여줘야한다.

그렇다고 안붙여주면 컴파일이 안되는 것은 아니고 경고가 발생한다.

Box b = new Box(); // 타입변수를 사용하지 않아 Object로 자동조정
b.setItem("Hello"); // 경고 발생 unchecked or unsafe operation

Box<String> b = new Box<T>();
b.setItem("Item"); // 경고발생 x 

또한, 다른타입으로 생성이 불가능하다.

Box<Apple> appleBox = new Box<Grape>(); // 선언하는 타입과 생성자에 대입된 타입이 일치 x

그러면 상속관계에서는 괜찮지 않을까 하는데 경기도 오산이다.

FruitApple이 상속했다고해도 생성이 불가능하다.

하지만 대입된 타입이 같은 경우는 가능하다.

Box<Fruit> box = new Box<Apple>(); //불가능

//대입된 타입이 같은경우는 가능
Box<Apple> box = new FruitBox<Apple>();

또한, 당연히 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

하지만, 상속관계에서는 가능하다. 객체 생성이 불가능 한거지 매개변수는 가능하다.

//대입된 타입과 다른 타입의 객체 추가 x
Box<Apple> box = new Box<Apple>();
box.add(new Apple()); // 가능
box.add(new Grape()); // 불가능

// 상속관계에서 가능 예시
Box<Fruit> box = new Box<Fruit>();
box.add(new Fruit());
box.add(new Apple());

지네릭스의 용어정리

예를들어 Box<T>라는 지네릭 클래스가 존자한다고 한다.

Box<T>지네릭 클래스라고 불린다 또한, T타입 변수 or 타입 매개변수라고 불린다.

Box원시 타입으로 불린다.

지네릭스의 제한

지네릭스는 제한되는 점이 있다.

  1. static멤버에 타입 변수를 사용할 수 없다.

만약 static멤버에 타입 변수를 사용한다면 인스턴스변수로 간주된기 떄문이다.

static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.

  1. 지네릭타입의 배열을 선언할 수 없다.

왜냐하면 new연산자 떄문이다. new는 컴파일 하는 시점에는 T가 어떤타입인지 알아야하지만 컴파일 하는 시점에서는 T가 어떤타입인지 알 수 없기 때문이다.

이러한 지네릭 클래스는 타입변수 T가 제한이 되있지 않아서 아무 타입종류나 다 들어갈 수 있다.

이러한 타입변수를 제한할 수 있는 방법은 없을까?

해결방법은 extends를 사용하면된다. 이를 사용하면 특정 타입의 자식들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit>{ // Fruit의 자식만 타입으로 지정할 수 있다.
    /...
}

또한, 클래스가아닌 인터페이스도 포함한다는 제약이 필요하다면 implements가 아닌 그냥 extends를 사용해서 해결하면 된다. <T extends Eatable> 이런식으로 사용하면 된다.

만약, 클래스의 자손이면서 인터페이스도 구현하려면 &를 붙여주면 해결된다. <T extends Eatable & Fruit> 이렇게 하면 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수에 대입될 수 있다.

와일드 카드

static메서드에서는 타입 매개변수를 매개변수로 사용할 수 없기 때문에 도입하였다.

class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox); // 가능하다.
Juicer.makeJuice(appleBox); // 불가능하다. 형변환을 할 수없기 때문이다.

이를 해결하면 그냥 makeJuice라는 메소드를 오버로딩하면 되지 않겠냐 생각할 수도 있지만 이것도 경기도 오산이다.

왜냐하면 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.

그래서 Apple타입을 오버로딩을 함부로 할 수 없다.

이를 해결하기 위해서 나온것이 바로 와일드 카드이다.

사용하는법은 다음과 같다.

  • <? extends T> : 와일드 카드의 상한 제한, T 와 그 자식들만 가능
  • <? super T> : 와일드 카드의 하한 제한, T 와 그 부모들만 가능
  • <?> : 제한 없음. 모든타입이 가능하다. == <? extends Object>와 동일하다

또한, 특징이 있는데 와일드 카드에서는 &를 사용할 수 없다는 것이 특징이다.

지네릭 메서드

지네릭 메서드는 메서드의 선언부에 지네릭 타입이 선언된 메서드를 말한다.

static <T> void sort<List<T> list, Comparator<? super T> c)

지네릭 메서드는 굳이 지네릭 클래스가 아닌 클래스에서도 정의될 수가 있다.

그리고 static멤버와 차이점이 있는데 staic멤버는 타입 매개변수를 사용할 수 없지만 지네릭 타입을 메서드에 선언하고 사용하면 가능하다.

또한, 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 static이건 아니건 상관이 없다.

예시로 makeJuice()를 지네릭 메서드로 바꾸면 다음과 같이 변경된다.

static Juice makeJuice (FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

// 지네릭 메서드로 변환
static <T extends Fruit> Juice makeJuice (FruitBox<T> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

이러한 메서드를 호출할 떄는 반드시 타입변수에 타입을 대입하고 호출해야한다.

지네릭 타입의 형변환

지네릭타입은 non지네릭 타입간의 형변환은 항상 가능하다. 하지만 경고가 발생하긴한다.

Box<Object> objBox = null;
Box box = null;

box = (Box)objBox; // 가능 -> 경고발생
objBox = (Box<String>)box; //가능 -> 경고발생

하지만 대입된 타입이 다른 지네릭타입간의 형변환은 불가능하다.

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; //에러발생 
strBox = (Box<String>)objBox; // 에러발생

만약 와일드카드 지네릭 타입에서 지네릭타입으로의 형변환을 어떨까?

결론적으로말하면 서로 형변환이 가능하다.

또한, 와일드카드 지네릭 타입에서 와일드카드 지네릭 타입으로의 형변환도 가능하다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 쓰레드 (1)  (0) 2021.09.24
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

DateCalendar가 가진 문제점을 해결하기 위해서 JDK.18부터 java.time패키지가 추가되었다.

이안에는 총 4개의 하위 패키지로 구성되어 있다.

  • java.time
    • 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
  • java.timechrono
    • 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
  • java.time.temporal
    • 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
  • java.time.zone
    • 시간대와 관련된 클래스들을 제공

Java.time패키지의 핵심 클래스

java.time패키지의 특징으로는 날짜와 시간을 별도의 클래스로 분리해 놓았다.

그래서 다음과 같이 사용한다.

  • 시간 : LocalTime클래스
    • 시간 - 시간 (기간) : Duration클래스
  • 날짜 : LocalDate클래스
    • 날짜 - 날짜 (기간) : Period클래스
  • 시간 + 날짜 : LocalDateTime클래스
  • 시간대 + 시간 + 날짜 : ZonedDateTime

이러한 클래스들을 사용하기 위해 객체를 생성해야하는데 생성하는 법은 다음과 같다.

LocalDate date = LocalDate.now(); // new를 사용하지 않음

LocalDate date = LocalDate.of(2021,09,18);

이런식으로 now()of()를 사용해서 객체를 생성할 수 있다. LocalDate를 예로 들었지만 다른 클래스들도 이런방식으로 생성하면 된다.

이러한 now()of()static메서드이다.

인터페이스의 종류

Temporal과 TemporalAmount

Temporal은 보통 날짜와 시간을 위한것이라고 생각하면 좋다.

  • Temporal, TemporalAccessor, TemporalField 를 구현한 클래스
    • LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant ...
  • TemporalAmount를 구현한 클래스
    • Period, Duration

TemporalUnit과 TemporalField

  • TemporalUnit: 날짜와 시간의 단위를 정의해 놓은 인터페이스, 열거형 ChronoUnit으로 구성되어 있다.
  • TemporalField : 년, 월, 일 등 날짜와 시간의 필드를 정의해 놓은 것, 이것도 열거형 ChronoField가 이 인터페이스를 구성하였다.

다음과 같은 예시를 보면 이해가 빠르다. 다음 예시는 특정 필드의 값을 얻기위한 매서드인 get()과 날짜와 시간을 빼거나 더하는 plus()에 대한 정의 이다.

int get(TemporalField field)
LocalDate plus(long amountToAdd, TemporalUnit, unit);

이를 TemporalField인지 TemporalUnit에서 사용할 수 있는 열거형인지 판단하는 방법은 다음과 같다.

boolean isSupported(TemporalField field);
boolean isSupported(TemporalUnit unit);

LocalDate와 LocalTime

LocalDateLocalTime은 날짜와 시간을 나타낸다.

이러한 값들을 생성하는 of()now()메서드는 static메서드로 특히 of()에는 다양한 매개변수가 오버로딩 되어있다.

LocalTime을 예로 들면 시간, 분, 초 뿐만이 아니라 나노초도 포함이 된다.

또한, 이들의 특징으로는 LocalTime을 예로들자면 만 단위로 지정해서 넣어줄 수 도 있다.

LocalTime time = LocalTime.ofSecondDay(86399); // 23시 59분 59초

또한, 시간과 표를 나타내는 것을 여러가지 방법이 있을 텐데 parse()메서드를 통해서 문자열을 날짜로 변환할 수 있다.

LocalDate date = LocalDate.parse("1999-03-16"); // 1999년 3월 16일
LocalTime time = LocalTime.parse("23:59:59); // 23시 59분 59초

get메서드

보통 Java에서는 특정 필드를 가져올 때는 get() 특정필드를 수정하거나 값을 변경할 때는 set()을 사용한다.

물론 LocalDateLocalTime에도 각 시간, 분, 초 이러한 단위들을 가져오는 get()함수들이 다량 존재한다.

인텔리제이나, 이클립스같은 IDE의 기능을 사용하여 살펴보자.

몇개만 소개하자면 다음과 같다.

LocalDate date = LocalDate.now();

date.getYear() // int형 년도 반환
date.getMonthValue() // Month형 월 반환
date.getMonth() // int형 월 반환
....

LocalTime time = LocalTime.now();

time.getHour()// int형 시간 반환
time.getMinute() // int형 분 반환
time.getSecond() // int형 초 반환
time.getNano() // int형 나노초 반환

지금은 get()만 사용했지만 기간이 너무 많거나 int형으로 표현이 안되는 값들은 getLong()을 사용하여 반환받는 것이 맞다.

ChronoField

이러한 get()을 하려할 때 메서드들의 매개변수로 사용할 수 있는 필드의 목록이다.

이는의 예시들을 다음과 같이 있다.

ERA // 시대
YEAR_OF_ERA , YEAR // 년
DAY_OF_WEEK // 요일 (1: 월요일, 2: 화요일)

MICRO_OF_SECOND * // 백만분의 일초
...

이러한 필드가 여러개가 있다. 사용할 때 주의해야할 점은

  • LocalDate는 날짜인데 사용하기 초를 나타내는 필드를 사용할거나 이러면 사용할 수 없다.
  • *이 표시되어 있는 필드는 int타입을 넘어가는 필드 이므로 getLong()을 사용해야한다.

필드의 값 변경하는법

필드의 값을 변경하기 위한 메서드로는 with(), plus(), minus()들이 존재한다.

날짜와 시간에서 특정 필드 값을 변경하려면, 다음과 같이 with로 시작하는 메서드를 사용하면 된다.

이러한 메서드들은 보통 새로운 객체를 생성해서 반환므로 대입연산자를 사용해야한다.

date = date.withYear(2000); // 년도를 2000년으로 변경한다.
time = time.withHour(12); // 시간을 12시로 변경

truncatedTo()메서드

이 메서드는 LocalTime에만 존재하며 LocalDate()는 존재하지 않는다.

왜냐하면 truncatedTo()는 짖ㅇ된 것보다 작은 단위의 필드를 모두 0으로 만드는 것이다.

LocalTime time = LocalTime.of(18, 12, 19); // 18시 12분 19초
time = time.turncatedTo(ChronoUnit.HOURS); // 18시 00분 00초가 된다.

이러한 기능이기 때문에 LocalDate()에서 만약 년도를 기준으로한다면 월,일이 0이되는데

0인 월, 일은 없기 때문이다.

날짜와 시간의 비교

LocalDateLocaltime의 날짜, 시간을 비교할 때는 compareTo()를 이용하면 되지만 그것보다 편리하게 비교할 수 있는 메서드 들이 존재한다.

boolean isAfter (ChronoLocalDate other); 
boolean isBefore (ChronoLocalDate other);
boolean isEqual (ChronoLocalDate other);

이중 isEqual메서드는 LocalDate에만 존재한다.

또한, isEqual은 오직 날짜만 비교한다. 한마디로 2018년 12월 19일이나 2019년 12월 19일은 같다는 것이다.

equals()는 모든 필드가 일치해야한다. 그러므로 2018년 12월 19일은 2019년 12월 19일은 다르다.

Instant

instant는 에포크 타임으로부터 경과된 시간을 나노초 단위로 표현한다.

이는 우리가 보기는 어렵다. (ex 1970-01-01 00:00:00 UTC)

하지만 단일 진법으로만 다루기 때문에 계산이 편하다.

생성하는 방법은 두가지 방법이있다. now()를 사용하는 것과 ofEpochSecond()를 사용하는 것과 같다.

Instant now = Instant.now(); 
Instant now = Instant.ofEpochSecond(now.getEposchSeocnd());
Instant now = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());

이러한 InstantDate로 변환되거 반대의경우고 다음과 같은 메서들ㄹ 통해 변환할 수 있다.

from(Instant instant); // Instant -> Date
toInstant() // Date -> Instant

LocalDateTime과 ZonedDateTime

LocalDateTimeZonedDateTime은 무언가 둘다 합쳐놓은 것이다.

  • LocalDateTime : LocalDate + LocalTime
  • ZonedDateTime : LoclaDateTime + 시간대

이렇게 구성이 되어있지만 이를 operand들을 합쳐서 결과를 생성할 있다.

물론 그냥 바로 생성해도 되지만 예를들자면 LocalDateTime으로 바꾸려고하는데 LocalDate는 현재 생성이 되있다면

LocalDate date = LocalDate.of(2018,12,19);

LocalDateTime dateTime = date.atTime(18,12,19); 

이런 방식으로도 생성할 수 있고 물론 반대도 존재하고 혹은 아무것도 없을 때, 아니면 둘다 있을 때 도 생성이 가능하다 여러가지 방법이 있으니까 참고하자

LocalDate date = LocalDate.of(2018, 12, 19);
LocalTime time = LocalTime.of(99, 03, 16);

LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt1 = date.atTime(time);
LocalDateTime dt2 = time.atDate(date);
LocalDateTime dt3 = date.atTime(12, 8, 14);
LocalDateTime dt4 = time.atDate(LocalDate.of(2018, 8, 14));
LocalDateTime dt5 = date.atStartOfDay();

반대로 LocalDateTimeLocalDate또는 LocalTime으로 변환할 수 있다.

LocalDateTime dt = LoclaDateTime.of(2018,12,19,99,3,16);
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

이를 LocalDateTime으로 ZonedDateTime으로 만들 수 있다.

왜냐하면 LocalDateTime + 시간대가 ZonedDateTime이기 때문이다.

LocalDateTime dt = LoclaDateTime.of(2018,12,19,12,3,16);

ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dt.atZone(zid); // 2018-12-19T12:03:16+09:00[Asia/Seoul]

이런식으로 atZone을 사용하여 시간대 정보를 넣어서 만들 수 있다.

이러한 ZonedDateTime을 이용하여 특정 시간대의 시간을 알기 위해서는 다음과 같이 하면된다.

ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

TemporalAdjusters

이를 사용하면 특정 날짜 계산을 사용할 수 있다.

plus() , minus()를 사용해서도 계산 할 수 있지만 특정년도의 3주차 월요일 이런것들을 물어보면 계산하기 복잡하다 이러한 TemporalAdjusters클래스를 용하여 편하게 계산 할 수 있다.

LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));

위와 같은 방식들을 사용해서 계산할 수 있다. 사용할 수 있는 것들은 다음것들 등이 있다.

firstDayOfNextYear() // 다음해의 첫 날
lastDayForYear() // 올 해의 마지막 날

firstInMonth (DayofWeek dayOfWeek) // 이번 달의 첫 번째 ? 요일
next (DayofWeek dayOfWeek) // 다음 ?요일(당일 미포함)

...

이러한 다양한 메소드들이 존재한다.

TemporalAdjusters직접 구현하기

이러한 TemporalAdjusters를 직접 구현할 수도 있다.

TemporalAdjuster인터페이스를 사용하면 쉽게 구현할 수 있다.

이 클래스는 다음과 같은 추상 메서드 하나만 정의되어 있고, 이 메서드만 구현하면 된다.

@FuntionalInterface
public interface TemporalAdjuster{
    Temporal adjustInto(Temporal temporal);
}

이를 오버라이딩해서 자유롭게 구현하면 된다.

Period와 Duration

Period는 날짜와의 차이를 나타내고

Duration은 시간의 차이를 계산한다.

어떤 특정의 두 날짜와 시간을 구별하기 위해서는 between()이라는 메소드를 사용하면 편하다.

LocalDate date1 = LocalDate.of(2018,12,19);
LocalDate date2 = LocalDate.now();

Period pe = Period.between(date1, date2);

DurationPeriod에서 특정 필드의 값을 얻기위해서는 LocalDateLocalTime과 마찬가지로 get()을 사용한다.

하지만 이러한 값들을 Unit이나 Field를 가져올 때는 불안하거나 한정되어있다

그렇기 때문에 이를 안전하게 가져오기 위해서는 LocalTime이나 LocalDate로 변환해서 사용하는 것이 좋다.

이러한 PeriodDurationLocalTimeLocalDate처럼

of()with()메서드를 사용할 수 있고 또한, plus()처럼 사칙연산이나 isAfter()처럼 비교연산을 하거나 다른단위로 변환할 수 있는 메서드들이 존재하니 참고하면 좋을 것 같다.

파싱과 포맷

여기서의 파싱은 날짜와 시간을 원하는 형식으로 출력하고 해석하는 방법이다.

내가 원하는 타입으로 format()을 사용하여 형식을 정해줄 수도 있다.

이러한 종류는 다양하니 java api를 찾아보는 것도 좋을 것같다

문자열을 날짜와 시간으로 파싱하기

문자열을 날짜 또는 시간으로 변환하기 위해서는 static메서드인 parse()를 사용하면 가능하다.

이는 오버로딩된 메소드가 여러개가 존재한다. 예제를 보자

Local date = LocalDate.parse("2016-01-02", DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter에 저장된 형식을 사용하여 정의된 형식으로 출력할 수도 있다.

또한, ofPattern()을 사용하여 파싱을 할 수도 있다.

ofPattern을 사용하면 원하는 형식에 따라 날짜 시간 분을 읽어올 수 있다.

DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

Refernece

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 쓰레드 (1)  (0) 2021.09.24
[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

+ Recent posts