Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5주차](박준영, 양재승, 임주민) #3

Open
sunwootest opened this issue Jul 25, 2023 · 3 comments
Open

[5주차](박준영, 양재승, 임주민) #3

sunwootest opened this issue Jul 25, 2023 · 3 comments
Assignees
Labels

Comments

@sunwootest
Copy link
Collaborator

  • 5주차
    • 1장
    • 2장
    • 3장
@farmJun
Copy link

farmJun commented Jul 30, 2023

📚토비의 스프링 2장 테스트

1️⃣ 왜 테스트를 해야하는가?

시간이 흐를수록 애플리케이션은 쉴 틈 없이 변하고, 복잡해진다.
이러한 변화에 대응하기 위한 두 가지 전략은 다음과 같다.

  1. 확장과 변화를 고려한 객체지향적 설계 및 IoC/DI
  2. 만들어진 코드에 대한 확신을 주고, 변화에 유연하게 대처할 수 있도록 도와주는 테스트 기술

테스트는 객체지향과 더불어 스프링이 개발자에게 제공하는 가장 중요한 가치이다.
그러니 테스트를 하지 않는 것은 스프링의 지닌 가치의 절반을 포기하는 것과 같다.
또, 테스트는 스프링을 학습하는 가장 효과적인 방법의 하나이다.

2️⃣ 테스트의 유용성

여러 가지 기능을 구현하는 과정에서 기능이 잘 동작하는지 어떻게 확인해야 할까?
프로그램을 실행시켜서 직접 하나하나 값을 입력하거나, 머릿속으로만 시뮬레이션하는 것은 올바르지 않음이 분명하다.
또, 잘 작동했던 기능에 요구사항이 변경되어 코드상의 많은 변화가 생겼을 때, 이 기능이 이전과 동일한 기능을 정상적으로 수행함을 어떻게 보장할 수 있을까?

바로 테스트이다. 즉, 테스트는 개발자의 의도대로 코드가 정확히 작동하는지 확인하여, 확신을 주는 작업이다.
테스트하며 코드와 설계의 결함을 사전에 알아챌 수 있고, 테스트가 성공하면 결함이 제거됐다고 확신할 수 있다.

3️⃣ 웹을 통한 DAO(Data Access Object) 테스트 방법의 문제점

보통 웹 프로그램에서 사용하는 DAO를 테스트하기 위해서, DAO, 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 구현한다.
그 후, 웹 화면을 띄워 폼을 열고, 값을 입력한 뒤, 버튼을 눌러 서버에 제대로 데이터가 전송되는지, 화면은 잘 변환되는지 확인한다.
(실제로 실습 과제 때 저의 모습이 이랬습니다.)

이러한 방법이 가장 흔한 방법이지만 "DAO에 대한" 테스트라고 말하기엔 너무 많은 외부 요인이 존재한다.
DAO에 대한 테스트를 실행하기 위해, 서비스 클래스, 컨트롤러, JSP 뷰 등 많은 클래스와 코드가 관여한다.
만약에 특정 기능에서 에러가 발생하였고 DAO의 문제가 아니라면, 에러가 어디서 발생했는지 찾기가 굉장히 어렵다.
DAO를 테스트하고 싶었지만, 다른 계층의 코드와 컴포넌트, 심지어 서버 설정 상태까지 확인해 봐야 한다.

즉, 이런 방식의 테스트는 번거롭고, 오류가 발생했을 때 신속하고 정확하게 대응하기 여렵다는 문제점이 있다.

4️⃣ 작은 단위의 테스트

"3. 웹을 통한 DAO(Data Access Object) 테스트 방법의 문제점"의 내용은 "DAO에 대한" 테스트틀 하고 싶었지만
사실 DAO를 포함해 서비스 클래스, 컨트롤러, JSP 뷰, DB 등 다양한 것을 동시에 테스트한 것과 같다.
해당 방식의 문제점에서 우리는, 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다고 알 수 있다.
관심사의 분리를 통헤 테스트틀 최대한 작은 단위로 쪼개야 한다.

이렇게 작은 단위에 대한 테스트를 단위 테스트(Unit Test)라고 한다.
여기서 말하는 "단위"는 구체적이지 않지만, 일반적으로 작을수록 좋다.

5️⃣ 자동수행 테스트 코드

매번 테스트를 할 때마다 개발자가 직접 값을 입력하고, 버튼을 클릭하고, 이 과정이 반복된다면 개발자에겐 거의 고문에 가까울 것이다.
개발자는 게을러야 한다. 그렇기에 자동으로 수행되는 테스트 코드를 작성하는 것은 중요하다.

가장 간단하게 테스트 할 수 있는 방법은 해당 자바 클레스의 main() 메서드를 실행하는 것이다. 번거롭게 입력할 필요도 없고, 서버를 띄우고, 브라우저를 열 불편함이 없다.
단지 main() 메서드를 실행하는 시간 0.xx초면 충분하다.
즉, 자동으로 수행되는 테스트 코드의 장점은 자주 반복할 수 있다는 것이다. 하루에 테스트를 몇백 번 해도 십 분 정도면 충분할 것이다.

서비스 운영 중에 어떠한 기능에 변경이 불가피할 때, 이 변경이 서비스에 문제를 일으키지 않을까 하는 걱정은 당연하다. 하지만, 이전에 해당 기능에 대하여 만들어준 자동수행 테스트가 있다면, 더욱 빨리 변경된 코드에 대한 확신을 얻을 수 있다.

6️⃣ JUnit 테스트

"5. 자동수행 테스트 코드"에서 언급한 main() 메서드로 만든 테스트는 가장 간단한 만큼 한계가 존재한다.
main() 메서드를 이용한 테스트만으로는 점점 커지는 애플리케이션의 규모를 테스트하기엔 부담이 늘어갈 것이다.

이미 자바에는 JUnit이라는 자바 테스팅 프레임워크가 존재한다. 이름 그대로 자바를 단위 테스트하는데 유용하다.

JUnit을 사용하기 위해서는 먼저 main() 메서드에 있는 테스트 코드들을 일반 메소드로 옮겨야 한다.
이때, JUnit이 전통적으로 public 메서드만을 테스트 메서드로 허용하기 때문에,메서드는 public으로 선언되어야 하고, 메소드에 @Test라는 어노테이션을 붙여야 한다.
@Test라는 어노테이션이 JUnit에게 해당 메서드가 테스트용 메서드임을 알려준다.

Scanner sc = new Scanner(System.in);

Something a = someFunction("same parameter");
Something b = someFunction("same parameter");

if (a.equals(b)) {
    Sytem.out.print("일치");    
} else {
    Sytem.out.print("불일치");
}

main() 메서드로 테스트를 한다면 위와 같은 코드의 형태였을 것이다.
이를 JUnit이 제공하는 assertThat()이라는 스태틱 메서드를 이용하면 다음과 같이 변경할 수 있다.

assertThat(a, is(b));

assertThat() 메서드는 첫 번째 파라미터의 값을 뒤에 나오는 매처(matcher)라는 조건으로 비교하여 일치하면 다음으로 넘어가고, 아니면 테스트를 실패하도록 한다.
is() 메서드는 매처의 일종으로 equals() 메서드로 비교해주는 기능을 가진다.

가독성이 훨씬 뛰어나다. assert that, 무엇을 주장한다.
a is b, a가 b라고, a와 b가 같다고.
즉, a와 b가 같은지 주장한다. 이 주장이 맞으면 테스트는 통과할 것이고, 그렇지 않다면 실패할 것임이 글처럼 읽힌다.

7️⃣ 실패하는 테스트
"테스트가 성공한다"하면 대부분은 구현한 기능이 정상적으로 작동한다라고만 생각한다. 하지만, 개발자는 실패하는 테스트도 작성해야한다. 개발자가 테스트를 만들 때 자주 하는 실수가 바로 성공하는 테스트만 만드는 것이다. 일반적으로 코드가 정상적으로 작동하는 경우를 상상하면서 테스트를 만들기 때문에 그렇다. 그래서 테스트를 만들 때, 실제로 발생할 수 있는 교묘한 상황을 잘 피해서 코드를 만든다.

스프링의 창시자인 로드 존슨은 "항상 네거티브 테스트를 먼저 만들라"라는 조언을 했다. 개발자는 한시라도 빨리 테스트를 성공하여 다음 기능으로 나아가고 싶기 때문에,
긍정적인 경우만을 골라서 테스트를 작성하게 되기 쉽다. 그래서 부정적인 테스트를 먼저 만드는 습관을 들이는 것이 좋다. 부정적인 테스트를 먼저 작성하게 되면, 예외적인 상황을 빠뜨리지 않고 더욱 꼼꼼하게 개발할 수 있다는 장점이 있다.

@Test(expected = IllegalArgumentException.class)
public void failureTest() throws Exception{
    //
    // test code
    //  
}

위 코드는 failureTest() 메서드가 실행됐을 때, @Test 뒤 expected에 적힌 IllegalArgumentException.class가 발생하면 성공하는 것이다.

@Test
public void failureTest() throws Exception{
   assertThrows(IllegalArgument.class,() -> {
        //test code
        });
}

또 다른 방법으로 assertThrows() 메서드를 사용하는 방법이 있다. 위 코드는 failureTest() 메서드가 실행됐을 때, 해당 테스트 코드 실행시 IllegalArgumentException.class가 발생하면 성공하는 것이다.

8️⃣ 테스트 주도 개발(TDD)

일반적으로 기능을 구현한다면 먼저 코드를 작성한 후에 작성된 코드를 검증하기 위해 테스트를 만드는 순서로 개발이 진행된다. 반대로 만들고자 하는 기능의 내용을 담고 있ㅇ면서
만들어진 코드를 검증도 해줄 수 있도록 테스트를 먼저 만든 다음, 테스트를 성공하게 해주는 코드를 작성하는 프로그래밍 방법론이 존재한다.
바로 테스트 주도 개발(Test Driven Development)이다.이는 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법이다. "실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다"는 것이 TDD의 기본 원칙이다.
이 원칙을 잘 따랐다면 만들어진 모든 코드는 빠짐없이 테스트로 검증됐다는 뜻이다.

9️⃣ 테스트 코드 개선
public class someTest{
    
    @Test
    public someTest1() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        Something something = context.getBean("something", Somethig.class);

        /// test code 1
    }

    @Test
    public someTest2() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        Something something = context.getBean("something", Somethig.class);

        /// test code 2
    }

    @Test
    public someTest3() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        Something something = context.getBean("something", Somethig.class);

        /// test code 3
    }
}

위 코드를 보면 무엇인가 냄새가 나지 않는가? 바로 중복되는 코드가 존재한다는 것이다. 중복된 코드는 별도의 메서드로 추출하는 것이 가장 간단한 방법이다. 하지만, 일반적인 메서드 추출 방식이 아닌 JUnit이 제공하는 기능을 활용해보자.
테스트를 실행할 때마다 반복되는 준비 작업을 별도의 메서드에 넣게 해주고, 이를 매번 테스트 메서드를 실행하기 전에 먼저 실행시켜주는 기능이다.

public class someTest{
    private Something something;
    
    @Before
    public void init(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        something = context.getBean("something", Somethig.class);
    }
    @Test
    public someTest1() {
        /// test code 1
    }

    @Test
    public someTest2() {
        /// test code 2
    }

    @Test
    public someTest3() {
        /// test code 3
    }
}

중복된 코드를 init()이라는 메서드에 넣는다. 그리고 something 변수를 인스턴스 변수로 변경한다. 그리고 @Before 어노테이션을 메서드 위에 작성해주면 된다.

🔟 JUnit 진행 과정

JUnit 프레임워크가 테스트 메서드를 실행하는 과정을 알아보자. 그전에 프레임워크가 무엇인지 알아야한다. 프레임워크는 스스로 제어권을 갖고 주도적으로 동작하고, 개발자가 만든 코드는 프레임워크에 의해 수동적으로 실행된다.
그래서 프레임워크에 사용되는 코드만으로는 실행 흐름이 잘 보이지 않기 때문에 프레임워크가 어떻게 사용할지를 잘 이해하고 있어야 한다.

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before가 붙은 메서드가 있으면 실행한다.
  4. @Test가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해준다.
  5. @After가 붙은 메서드가 있으면 실행한다.
  6. 나머지 테스트 메서드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

@sheepseung
Copy link

토비의 스프링 정리

Chapter 1. 오브젝트와 의존관계

  • 📗관심사의 분리

    관심사의 분리

    “객체지향 세계에서는 모든 것이 변한다.” 이 때 변한다는 의미는 프로젝트의 설계와 이를 구현한 코드가 변한다는 뜻이다. 소프트웨어 개발에서는 영구란 개념과 끝이란 개념은 없다.

    즉 우리는 죽을때까지 유지보수에 신경을 써야하고, 우리가 공부하는 모든 과정은 결국 이 유지보수를 효율적으로 하기 위해 공부하는 것이다.

    이를 위해 처음으로 나오는 내용이 바로 프로그래밍의 기초 개념인 **“관심사의 분리”**이다.

    책에서 예제로 제시하는 코드로 DB와 연동한 UserDao에 대해서 출발한다. ‘초난감 UserDao’ 클래스는 한 클래스 내에서 DB Conection뿐 아니라 데이터를 관리하는 메인 로직, 리소스 관리 등 여러가지 관심사를 한 클래스 내에 모두 구현해 놓았다. 이는 관심사 분리가 이루어 지지 않은 상태이다.

    책에서는 “디비 커넥션”이라는 단일 관심사를 분리하는 방법을 단계별로 리팩토링 과정을 보여주며 제시한다.

    -메소드 추출

    책에서는 이를 getConnection()함수의 추출로 예를 든다. 클래스 내에 add(), get()등 디비 커넥션을 필요로 하는 멤버함수마다 getConnection()함수를 필요하므로 이를 따로 추출하여 구현해 놓으면 중복 코드를 없앨 수 있게된다.

    -상속을 통한 확장

    UserDao클래스를 만들고 싶은 인스턴스(NUserDao, DUserDao)에 각각 상속받아 getConection을 각 인스턴스에 맞게 오버라이딩하여 확장을 하는 방법을 제시하지만 상속을 통한 확장엔 치명적인 한계가 있다.

    • 자바는 다중상속을 허용하지 않음
    • 여전히 긴밀한 결합을 허용 (부모 클래스 변경시 자식 클래스에 영향을 끼침)
    • 확장 기능을 다른 DAO 클래스에 적용할 수 없음
  • 📗클래스 분리

    클래스 분리

    -클래스의 분리

    위에서 나온 관심사의 분리를 이번엔 클래스의 분리로 적용한다. 즉 하나의 클래스는 하나의 관심사를 다루도록 분리한다.

    분리한 클래스를 연결하기 위해 생성자에서 클래스를 연결해 주었다.

    public class UserDao{
    private SimpleConnectionMaker simpleConnectionMaker;
    
    public UserDao(){
    	simpleConnectionMaker = new SimpleConnectionMaker();
    }
    
    public void add(User user) //(생략...)
    	Connection c = simpleConnectionMaker.makeNewConnection();
    	//생략
    }
    
    public User get(Sting id) //생략
    	Connection c = simpleConnectionMaker.makeNewConnection();
    	//생략
    }	 

    우리는 이제 몇 주 전과 다르게 위 코드의 문제점을 알고 있다. 관심사를 각각의 클래스로 분리했지만, 두 클래스 간에 관계가 생겨버렸다. 이를 책에서는 UserDao클래스가 SimpleConnectionmaker 클래스를 ‘알고 있다’고 표현한다. 즉 클래스 안에 해당 코드가 존재해서는 안된다고 표현한다.

    관계가 생겼을 시에 문제점은 다음과 같다.

    • 자유로운 확장이 불가
    • DB 커넥션을 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고 있어야 함

    즉 만약 우리가 ConnectionMaker를 바꾸었을 때 UserDao클래스로 직접 들어가서 하나하나씩 바꿔줘야 합니다. 이를 관계가 생겼다고 합니다.

    -인터페이스 적용

    위 문제점을 극복하기 위해 관계를 풀어줄 인터페이스가 등장한다.

    인터페이스를 사용하여 다형성을 활용하면 ConnectionMaker()에 확장성이 생긴다.

    하지만 여전히 UserDao 생성자에서 오브젝트를 생성해야하기 때문에 근본적인 문제 해결이 되지 않는다.

    public class DConnectionMaker implements ConnectionMaker{
    	public Connection makeConnection() //생략{
    		//D사의 Connection 생성 코드
    	}
    }
  • 📗의존관계 주입(DI)

    의존관계 주입(DI)

    드디어 등장하는 개념이 관계 주입이다. 이를 책에서는 ‘클래스가 아니라 오브젝트와 오브젝트 사이의 관계를 설정’해 주는 과정이라고 표현한다.

    즉 오브젝트 사이의 관계를 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 구현한다.

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

    이 때 관계설정 또한 하나의 관심사이기 때문에 이를 클라이언트의 책임으로 넘겨준다. 이 때 클라이언트는 Test클래스의 main()메소드로 표현한다.

    public class UserTest{
    	public static void main(String[] args) //throws 생략
    		Connection connectionMaker = new DConnectionMaker();
    		UserDao dao = new UserDao(connectionMaker);

    즉 userTest는 UserDao와 ConnectionMaker를 구현한 클래스와의 런타임 오브젝트 의존관계를 설정하는 책임을 담당한다.

    이제 우리는 어떤 종류의 ConnectionMaker 클래스가 새로 생겨도 main()클래스의 의존관계를 설정해주는 2줄 만 바꾸어 주면 된다.

  • 📗원칙과 패턴

    원칙과 패턴

    지금까지 정리한 관계주입의 구체적인 장점을 원칙과 패턴으로 정리해 보자.

    -개방 폐쇄 원칙(OCP)

    ‘클래스나 모듈은 확장에는 열려 있고 변경에는 닫혀 있어야 한다’

    위 예제를 보면 DB커넥션을 담당하는 클래스에 대하여 각각의 다른 커넥션의 확장에는 열려 있고, 이는 UserDao클래스에는 영향을 주지 않고 확장이 가능하므로 변경에는 닫혀있는 개방 폐쇄 원칙을 잘 지키고 있음을 알 수 있다.

    -응집도와 결합도

    초반 스터디 과제에서 선배님의 피드백에서 자주 보았던 ‘응집도는 높이고 결합도는 낮추자’는 말이 이제는 이해가 된다.

    응집도가 높다는 것은 프로젝트에 변화(주입해 줄 객체 변경)가 일어날 때 모듈에서 변하는 부분이 크다는 것을 의미한다.

    결합도는 책임과 관심사가 오브젝트 또는 모듈끼리의 연관관계를 뜻한다. 즉 결합도가 낮을수록 느슨하게 연결되어 변화에 대응(확장)하기 용이하고 구성이 깔끔한 코드가 된다.

    -전략 패턴

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

    위 예제 코드에서는 DB 커넥션을 인터페이스로 정의하고 이를 구현한 클래스를 전략을 바꿔가며 사용할 수 있게 분리한 것이다.

    전략패턴은 컨텍스트를 사용하는 클라이언트(UserDaoTest)가 사용할 전략(ConnectionMaker)을 컨텍스트의 생성자 등을 통해 제공해주는게 일반적이다.

  • 📗제어의 역전(IoC)과 오브젝트 펙토리

    제어의 역전(IoC)와 오브젝트 팩토리

    -오브젝트 팩토리

    객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 역할을 한다.

    위 예제에서는 UserDaoTest클래스에서 이를 수행했지만, 의미상 이는 프로젝트의 기능이 잘 동작하는지 Test하는 클래스이다. 따라서 오브젝트의 관계를 맺어주는 하나의 관심사를 수행하는 클래스를 따로 만들어줄 필요가 있다.

    public class DaoFactory{
     public UserDao userDao(){
    	ConnectionMaker connectionMaker = new DConnectionMaker();
    	UserDao userDao = new UserDao(connectionMaker);
    	return userDao;
    	}
    }
    public class UserDaoTest{
    	public static void main(String[] args){
    		//생략
    		UserDao dao = new DaoFactory().userDao();
    	}
    }

    팩토리는 단순히 관심사 분리만 의미하는 것은 아니다. 이를 활용하여 확장에서의 중복을 줄일 수 있다.

    public class DaoFactory{
    	public UserDao userDao(){
    			return new UserDao(connectionMaker());
    	}
    	public AccountDao userDao(){
    			return new UserDao(connectionMaker());
    	}
    	public MessageDao userDao(){
    			return new UserDao(connectionMaker());		
    	}
    
    	public ConnectionMaker connectionMaker(){
    			return new DConnectionMaker();
    	}
    }

    -제어의 역전(IoC)

    제어의 역전이란 쉽게 말해 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 할 수 있다.

    일반적인 코드의 흐름처럼 메소드를 따라 진행되는 흐름과 달리 지금까지 배운 내용은 추상화와 관계 주입을 통한 제어 권한의 위임이다.

    즉 관계 형성을 수동적으로 바꿈으로써 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되는 프로그램의 흐름이 뒤바뀌는 구조가 되는것이다.

    -라이브러리와 프레임워크의 차이점

    제어의 역전의 측면에서 이 둘의 차이점과 연관지어 이해할 수 있다.

    라이브러리는 애플리케이션의 흐름을 직접 제어한다. 단지 동작 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다.

    반면 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 따라서 프레임워크에는 분명한 제어의 역전 개념이 적용되어 있어야 한다.

  • 📗스프링의 IoC

    스프링의 IoC

    -애플리케이션 컨텍스트(스프링 컨테이너)의 동작방식

    스프링에서 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부른다. 이 빈 객체를 제어하는 IoC 오브젝트를 빈 팩토리(bean factory)라고 부르고 단순 빈 관리 외에 여러가지 기능을 제공하는 확장된 오브젝트를 애플리케이션 컨텍스트(applcation context)라고 부른다.

    그렇다면 이 오브젝트를 사용하는 방법과 방식을 정리해보자.

    먼저 설정정보로 사용할 클래스에 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @configuration 어노테이션을 추가한다. 그리고 오브젝트를 만들어주는 메소드에 @bean 어노테이션을 붙여준다. 이로써 스프링 컨테이너에 빈 객체를 올려놓을 준비가 된 것이다.

    이제 이 빈 객체를 사용하기 위해서 ApplicationContext의 만들고 AnnotationConfigurationContext 인터페이스를 통해 객체를 만들고 getBean()함수를 통해 빈 객체를 참조하여 사용하면 된다.

    그렇다면 오브젝트 팩토리로 직접 사용했을때와 비교하여 애플리케이션 컨텍스트를 사용했을 때에 얻을 수 있는 장점에는 무엇이 있을까?

    • 클라이언트가 구체적인 팩토리 클래스를 알 필요가 없다 ⇒ 팩토리 클래스의 변동과 상관 없이 일관된 방법으로 원하는 오브젝트를 가져올 수 있다.
    • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공한다 ⇒ 오브젝트를 효솨적으로 활용할 수 있는 다양한 기능을 제공한다. ex) 오브젝트 생성 방식 설정 가능, 이터셉팅 등

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

    스프링 애플리케이션 컨텍스트는 기존에 직접 만든 오브젝트 팩토리와 달리 동등성을 보장하는 싱글톤으로 빈 객체를 반환한다.

    즉 스프링은 여러번 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다. 이처럼 스프링은 직접 싱글톤 형태의 오브젝트를 만들과 관리하는 기능을 제공하는 싱글톤 레지스트리다.

    이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이란 것과 연관이 있다. 클라이언트에게 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어진다면 데이터 관리, 부하 등 여러가지 심각한 문제가 생길 것이다.

    하지만 경우에 따라 싱글톤 외의 스코프를 가져야 할 경우가 생길 수 있는데, 이는 프로토타입 스코프를 이용하여 구현할 수 있다.

    -IoC와 의존관계 주입

    여기까지 1장의 내용을 정리하면 스프링은 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 IoC컨테이너이다.

    이 때 객체관의 관계를 맺어 줄 때 DI방식을 사용한다. 또한 의존관계를 맺는 방법이 외부로 부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색방식으로 이루어 진다고 할 수 있다.

    이러한 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다. getBean() 메소드가 바로 이 의존관계 검색에 사용되는 것이다.

    의존관계 검색은 기존의 의존관계 주입의 모든 장점을 가지고 있으며 추가적으로 하나의 중요한 차이점을 가지고 있다. 바로 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링 빈일 필요가 없다는 점이다. 따라서 굳이 스프링에 빈으로 등록되지 않아도 빈 오브젝트를 사용할 수 있는 것이다.

  • DI를 사용한 IoC구현의 흐름 정리

    1. 관심사가 다른 코드를 클래스 단위로 분리
    2. 바뀔 수 있는 (주입 할)클래스는 인터페이스로 구현, 사용하는 (주입 받는)클래스에서 인터페이스를 통해서만 접근하도록 구현

@jumining
Copy link

jumining commented Aug 1, 2023

📜 Notion

예시코드 포함된 노션 바로가기

📖 3장 템플릿

[템플릿]

바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법 스프링에 적용된 템플릿 기법, 완성도 있는 DAO 코드를 만드는 방법

  • 3.1 다시보는 초난감 DAO

    3장 목표
    : 예외처리와 안전한 리소스 반환을 보장해주는 DAO 코드 작성
    : 이를 객체지향 설계 원리와 디자인 패턴, DI 등을 적용해 깔끔+유연+단순 코드로 작성

    /*
     예외처리 없는 deleteAll() (JDBC API 이용한 DAO 코드)
    */
    public void deleteAll() throws SQLException {
    	Connection c = dataSource.getConnection();
    	
    	PreparedStatement ps = c.preparedStatement("delete from users");
    	ps.executeUpdate(); // 두 줄에서 예외 발생하면 바로 메소드 실행 중단
    
    	ps.close();
    	c.close();
    }

    [예외처리 기능을 갖춘 DAO]

    사용한 리소스는 반드시 반환해야 함 Connection이나 PreparedStatement에 있는 close( ) 메소드는 리소스를 반환 오류 날 때마다 반환되지 못한 Connection 쌓이면 커넥션 풀에 여유 X, 리소스 모자라짐 → 서버 중단

    DB풀 : 매번 getConnection( )으로 가져간 커넥션을 명시적으로 close( )해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청 있을 때 재사용 가능

    /*
     예외처리 있는 deleteAll() - try/catch/finally 사용
    */
    public void deleteAll() throws SQLException {
    	Connection c = null;
    	PreparedStatement ps = null;
    
    	try {
    		c = dataSource.getConnection();
    		ps = c.preparedStatement("delete from users");
    		ps.executeUpdate(); 
    	} catch (SQLException e) {
    		throw e;
    	} finally {
    		if (ps != null) {
    			try { ps.close(); } catch (SQLException e) { }
    		}
    		if (c != null) {
    			try { c.close(); } catch (SQLException e) { }
    		}
    	}
    }
  • 3.2 변하는 것과 변하지 않는 것

    [JDBC try/catch/finally 코드의 문제점]

    예외처리블록이 2중으로 중첩되며 모든 메소드마다 반복 → 변하지 않고 많은 곳에서 중복되어 동일하게 나타나는 코드와 / 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업 시도

    [분리와 재사용을 위한 디자인 패턴 적용]

    1. 변하는 성격이 다른 것을 찾아내기
    2. 변하지 않는 부분을 재사용할 수 있는 방법

    [템플릿 메소드 패턴의 적용]

    상속을 통해 기능을 확장해서 사용 변하지 않는 부분은 슈퍼클래스에, 변하는 부분은 추상 메소드로 정의 서브 클래스에서 오버라이드하여 새롭게 정의해서 사용

    [템플릿 메소드 패턴의 문제점]

    DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 함 확장 구조가 이미 클래스를 설계하는 시점에서 고정 (서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있음) → 관계에 대한 유연성 떨어짐

    [전략 패턴의 적용]

    (개방폐쇄원칙(OCP) + 템플릿 메소드 패턴)보다 유연하고 확장성 뛰어남
    일정한 구조를 가지고 있다가 특정 확장 기능은 인터페이스를 통해 외부의 독립된 전략 클래스(변하는 부분을 별도의 클래스로 작성)에 위임하는 방식 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있음

    • 문제 : 컨텍스트가 인터페이스뿐 아니라 특정 구현 클래스를 직접 알고 있음
    /*
    전략패턴 사용 X
    */
    public void deleteAll() throws SQLException {
    	... 
    	try {
    		c = dataSource.getConnection();
    
    		ps = c.preparedStatement("delete from users");
    
    		ps.executeUpdate(); 
    	} catch (SQLException e) {
    	...
    }
    /*
    전략패턴 사용 0
    */
    public void deleteAll() throws SQLException {
    	... 
    	try {
    		c = dataSource.getConnection();
    		
    		// 컨텍스트가 인터페이스뿐 아니라 특정 구현 클래스(DeleteAllStatement)를 직접 알고 있음
    		StatementStrategy strategy = new DeleteAllStatement();
    		ps = strategy.makePreparedStatement(c);
    
    		ps.executeUpdate(); 
    	} catch (SQLException e) {
    	...
    }

    [클라이언트/컨텍스트 분리]

    클라이언트가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 컨텍스트에 전달하고, 컨텍스트는 전달받은 그 전략 구현 클래스의 오브젝트를 사용함

    [컨텍스트에 해당하는 부분을 별도의 메소드로 독립]

    클라이언트는 전략 클래스의 오브젝트를 컨텍스트의 메소드를 호출하며 전달해야함
    → 전략 인터페이스를 컨텍스트 메소드 파라미터로 지정할 필요

    /*
    [컨텍스트 코드] 
    메소드로 분리한 try/catch/finally
    클라이언트로부터 전략 오브젝트를 제공 받고 컨텍스트 내에서 작업을 수행
    */
    public void jdbcContextWithStatementStrategy(StatementStrategy stmt) 
    throws SQLException {
    	Connection c = null;
    	PreparedStatement ps = null;
    
    	try {
    		c = dataSource.getConnection();
    
    		ps = stmt.makePreparedStatement(c);
    
    		ps.executeUpdate(); 
    	} catch (SQLException e) {
    		throw e;
    	} finally {
    		if (ps != null) { try { ps.close(); } catch (SQLException e) { }}
    		if (c != null) { try { c.close(); } catch (SQLException e) { }}
    	}
    }
    /*
    [클라이언트 코드] 
    전략 오브젝트를 만들고 컨텍스를 호출하는 책임
    */
    public void deleteAll() throws SQLException {
    	StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스 오브젝트 생성
    	jdbcContextWithStatementStrategy(st); // 컨텍스트 호출, 전략 오브젝트 전달
    }
  • 3.3 JDBC 전략 패턴의 최적화

    DAO 메소드는 전략 패턴의 클라이언트로서 컨텍스트의 메소드에 적절한 전략, 즉 바뀌는 로직을 제공해주는 방법으로 사용 가능
    ex. 컨텍스트는 PreparedStatement를 실행하는 JDBC의 작업 흐름
    ex. 전략은 PreparedStatement를 생성하는 것

    [전략 클래스의 추가 정보]

    전략을 수행하기 위한 추가 정보는 생성자를 통해 제공받게함

    [전략과 클라이언트의 동거]

    기존 구조 문제점 :

    1. DAO 메소드마다 새로운 전략 구현 클래스를 만들어야 해서 클래스 파일 많아짐
    2. DAO 메소드에서 전략에 전달할 부가 정보가 있는 경우 이를 위해 오브젝트를 전달받는 생성자와 인스턴스 변수를 번거롭게 만들어야 한다는 점

    [로컬 클래스]

    해결 방법 → 전략 클래스를 매번 독립된 파일로 만들지 말고 클래스 안에 내부 클래스로 정의 클라이언트 메소드 내부에 구현클래스(로컬 클래스) 선언
    로컬 클래스 : 다른 클래스 내부에 정의되는 클래스(중첩 클래스) 중 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부클래스 중 메소드 레벨에 정의되는 클래스

    자신이 선언된 곳의 정보에 접근 가능 → 생성자를 통해 전달할 필요 X

    장점 : 메소드마다 추가해야 했던 클래스 파일 줄이기 가능, 로컬 변수를 바로 가져다 사용하기 가능

    [익명 내부 클래스]

    다른 클래스 내부에 정의되는 클래스(중첩 클래스) 중 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부클래스 중 이름을 갖지 않는 클래스(범위는 선언된 위치에 따라 다름)
    클래스 선언과 오브젝트 생성이 결합된 형태로 만들어짐
    클래스 재사용할 필요 없고 구현한 인터페이스 타입으로만 사용할 경우에 유용
    (위의 AddStatement 이름 제거)

    new 인터페이스 이름( )  { 클래스 본문 };
  • 3.4 컨텍스트와 DI

    [JdbcContext의 분리]

    jdbcContextWithStatementStrategy( )는 컨텍스트, 다른 DAO에서도 사용 가능

    [클래스 분리]

    DataSource에 의존하고 있으므로 DataSource타입 빈을 DI 받을 수 있도록 해주어야 함(생성자 방식)

    [빈 의존관계 변경]

    (기존) userDao 빈이 dataSource빈 직접 의존
    (현재) userDao빈이 jdbcContext 빈 의존, jdbcContext빈이 dataSource빈 의존

    [JdbcContext의 특별한 DI]

    스프링의 DI : 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적
    현재 JdbcContext는 인터페이스가 아닌 구체 클래스(구현 방법 바뀔 가능성 없으므로 인터페이스 X) → 클래스레벨에서 의존 관계 결정됨

    [JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유, 스프링 빈으로 등록해서 사용하는 이유]

    1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 때문 jdbcContext는 JDBC 컨텍스트 메소드를 제공해주는 일종의 서비스 오브젝트로서 싱글톤으로 등록되어 여러 오브젝트에서 공유해 사용되는 것이 이상적
    2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문 DataSource 오브젝트를 주입받음. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어야 함(스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있어서)

    [코드를 이용하는 수동 DI]

    JdbcContext를 스프링 빈으로 등록 X, UserDao 내부에서 직접 DI 적용하는 방식 UserDao가 DI 컨테이너처럼 동작하게 만들음

    • UserDao가 JdbcContext의 생성과 초기화를 책임지게 됨
    • JdbcContext는 다른 빈(DataSource)을 인터페이스를 통해 간접적으로 의존하고 있어 자신도 빈으로 등록되어야 함(의존 오브젝트를 DI를 통해 제공받기 위해) → UserDao에게 DI를 맡김. DataSource는 UserDao가 대신 DI 받게 수정
  • 3.5 템플릿과 콜백

    [템플릿/콜백 패턴]

    : 전략패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

    • 템플릿 : 전략 패턴의 컨텍스트(고정된 작업 흐름을 가진 코드를 재사용)
    • 콜백 : 익명 내부 클래스로 만들어지는 오브젝트(실행되는 것을 목적으로(값 참조만X) 다른 오브젝트의 메소드에 전달되는 오브젝트)

    [템플릿/콜백의 특징]

    콜백은 보통 단일 메소드 인터페이스 사용 (특정 기능을 위해 한 번 호출되는 경우가 일반적이라) 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어짐

    콜백 인터페이스의 메소드의 파라미터 : 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받을 때 사용 ex. Connection 오브젝트로 makePreparedStatement( ) 실행할 때 파라미터로 넘겨줌

    1. 클라이언트 : 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트 생성, 콜백이 참조할 정보 제공. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달
    2. 템플릿 : 정해진 작업 흐름을 따라 작업 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드 호출. 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보 이용해 작업 수행 후 결과를 템플릿에게 줌. 콜백이 돌려준 정보 사용해서 작업 마저 수행

    [콜백의 분리와 재활용]

    템플릿/콜백의 아쉬운 점
    : DAO 메소드에서 매번 익명 내부 클래스를 사용해서 코드 작성과 읽기가 불편해 익명 내부 클래스의 사용 최소화할 수 있는 방법 필요
    → SQL 문장만 파라미터로 받아서 바꿀 수 있게 하고 메소드 내용 전체를 분리해 별도의 메소드로 작성 시도

    [콜백과 템플릿의 결합]

    재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 됨

    구체적인 구현과 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등 기술은 감춰두고 외부에는 꼭 필요한 기능을 제공하는 단순한 메소드만 노출

    [템플릿/콜백의 응용]

    바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어지는 경우 활용
    자주 반복되는 코드가 있을 때 인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하도록 함

    1. 템플릿에 담을 반복되는 작업 흐름이 무엇인지
    2. 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇인지
    3. 콜백이 템플릿에게 돌려줄 내용은 무엇인지
    4. 템플릿이 작업을 마친 뒤 클라이언트에게 돌려줄 내용은 무엇인지
  • 3.6 스프링의 JDBCTEMPLATE

    스프링이 제공하는 템플릿/콜백 기술
    JdbcTemplate : 스프링이 제공하는 JDBC 코드용 기본 템플릿 생성자 파라미터로 의존 주입

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants