카테고리 보관물: Java

Spring 입문 (JPA에서의 동적 쿼리)

현재 상품 주문 시스템에서 검색 기능을 활성화 하기 위해서는 동적 쿼리를 해결해야한다.

방법 1. JPQL로 처리

public List<Order> findAllByString(OrderSearch orderSearch) {
        //language=JPAQL
        String jpql = "select o From Order o join o.member m";
        boolean isFirstCondition = true;

        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            if (isFirstCondition) {
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " o.status = :status";
        }

        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            if (isFirstCondition) {
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " m.name like :name";
        }
        TypedQuery<Order> query = em.createQuery(jpql, Order.class)
                .setMaxResults(1000); //최대 1000건
        if (orderSearch.getOrderStatus() != null) {
            query = query.setParameter("status",
                      orderSearch.getOrderStatus());
        }
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            query = query.setParameter("name", 
                      orderSearch.getMemberName());
        }
        return query.getResultList();
    }

JPQL 쿼리를 문자로 생성하기는 번거로움
실수로 인한 버그가 충분히 발생할 수 있음

방법 2. JPA Criteria로 처리

public List<Order> findAllByCriteria(OrderSearch orderSearch) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> o = cq.from(Order.class);
        Join<Order, Member> m = o.join("member", JoinType.INNER);
        List<Predicate> criteria = new ArrayList<>();

        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            Predicate status = cb.equal(o.get("status"),
                    orderSearch.getOrderStatus());
            criteria.add(status);
        }

        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            Predicate name =
                    cb.like(m.<String>get("name"), "%" +
                            orderSearch.getMemberName() + "%");
            criteria.add(name);
        }
        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000);
        return query.getResultList();
    }

JPA Criteria는 JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡
결국 다른 대안이 필요
해결책 ==> Querydsl 제시

방법 3. Querydsl로 처리

public List<Order> findAll(OrderSearch orderSearch){
        QOrder order = QOrder.order;
        QMember member = QMember.member;

        return query
                .select(order)
                .from(order)
                .join(order.member, member)
                .where(statusEq(orderSearch.getOrderStatus())),
                        nameLink(orderSearch.getMemberName()))
                .limit(1000)
                .fetch();
    }

    private BooleanExpression statusEq(OrderStatus statusCond){
        if(statusCond == null){
            return null;
        }
        return  order.status.eq(statusCond);
    }

    private BooleanExpression nameLink(String nameCond){
        if (!StringUtils.hasText(nameCond)){
            return null;
        }
        return member.name.link(nameCond);
    }

훨씬 간결한 방법으로 동적 쿼리 문제를 해결
dsl에 대한 학습이 필수적으로 필요해 보임

thymeleaf 타임리프 적용(스프링입문 html)

간단한 html파일에 타임리프를 적용해 보았다.
기존 코드는 그대로 두고 th:를 통해 덧대었다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="utf-8">
 <link href="../css/bootstrap.min.css"
 th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
 <div class="py-5 text-center">
 <h2>상품 목록</h2>
 </div>
 <div class="row">
 <div class="col">
 <button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
 th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품 등록</button>
 </div>
 </div>
 <hr class="my-4">
 <div>
 <table class="table">
 <thead>
 <tr>
 <th>ID</th>
 <th>상품명</th>
 <th>가격</th>
 <th>수량</th>
 </tr>
 </thead>
 <tbody>
 <tr th:each="item : ${items}">
 <td><a href="item.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
 <td><a href="item.html" th:href="@{|/basic/items/${item.id}|}"
th:text="${item.itemName}">상품명</a></td>
 <td th:text="${item.price}">10000</td>
 <td th:text="${item.quantity}">10</td>
 </tr>
 </tbody>
 </table>
 </div>
</div> <!-- /container -->
</body>
</html>

타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">


속성 변경 – th:href
th:href="@{/css/bootstrap.min.css}"
href="value1"th:href="value2" 의 값으로 변경
타임리프 뷰 템플릿을 거치면 원래 값을 th:xxx 값으로 변경. 만약 값이 없다면 새로 생성
HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 의 값이 href
대체되면서 동적으로 변경 가능
대부분의 HTML 속성을 th:xxx 로 변경 가능


타임리프 핵심
핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용
HTML을 파일로 직접 열었을 때, th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시
따라서 HTML을 파일 보기를 유지하면서 템플릿 기능 가능


URL 링크 표현식 – @{…},
th:href="@{/css/bootstrap.min.css}"
@{…} : 타임리프는 URL 링크를 사용하는 경우 @{…} 를 사용. 이것을 URL 링크 표현식
URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함


상품 등록 폼으로 이동
속성 변경 – th:onclick
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
여기에는 다음에 설명하는 리터럴 대체 문법이 사용


리터럴 대체 – |…|
|…| :이렇게 사용
타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 함

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용 가능

<span th:text="|Welcome to our application, ${user.name}!|">

결과를 다음과 같이 만들어야 하는데
location.href='/basic/items/add'
그냥 사용하면 문자와 표현식을 각각 따로 더해서 사용해야 하므로 다음과 같이 복잡해짐
th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
리터럴 대체 문법을 사용하면 다음과 같이 편리하게 사용 가능
th:onclick="|location.href='@{/basic/items/add}'|"


반복 출력 – th:each

<tr th:each="item : ${items}">


반복은 th:each 를 사용. 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용 가능
컬렉션의 수 만큼 <tr>..</tr>이 하위 테그를 포함해서 생성


변수 표현식 – ${…}

<td th:text="${item.price}">10000</td>

모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회 가능
프로퍼티 접근법을 사용 ( item.getPrice() )


내용 변경 – th:text

<td th:text="${item.price}">10000</td>

내용의 값을 th:text 의 값으로 변경
여기서는 10000을 ${item.price} 의 값으로 변경


URL 링크 표현식2 – @{…},

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"

URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용 가능
경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성
예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
생성 링크: http://localhost:8080/basic/items/1?query=test


URL 링크 간단히
th:href="@{|/basic/items/${item.id}|}"
리터럴 대체 문법을 활용해서 간단히 사용 가능

참고
타임리프는 순수 HTML을 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다. JSP를 생각해보면, JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다. 오직 서버를 통해서 JSP를 열어야 한다.
이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)이라 한다.

Spring 입문4

  1. 컴포넌트 스캔
    1-1 컴포넌트 스캔과 의존관계 자동 주입 시작하기
    – 기존의 @Bean 방식을 @Component 방식으로 변경
    – AppConfig에 @Configuration과 @ComponentScan 애노테이션을 설정
    – 이 때 @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록
    – 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 각각 붙임
    – ServiceImpl등 생성자로 Repository 따위를 전달 받는 곳에 @Autowired를 통해 의존관계를 자동으로 주입 받음 (스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입)

    1-2 기본 스캔 대상
    – 컴포넌트 스캔은 @Component 뿐만 아니라 아래 내용도 추가로 대상에 포함
    — @Component : 컴포넌트 스캔에서 사용
    — @Controlller : 스프링 MVC 컨트롤러에서 사용 (스프링 MVC 컨트롤러로 인식)
    — @Service : 스프링 비즈니스 로직에서 사용 (스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환)
    — @Repository : 스프링 데이터 접근 계층에서 사용 (스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리)
    — @Configuration : 스프링 설정 정보에서 사용

    1-3 필터
    – includeFilters : 컴포넌트 스캔 대상을 추가로 지정
    – excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정
    – FilterType 옵션
    — ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
    ex) org.example.SomeAnnotation
    — ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
    ex) org.example.SomeClass
    — ASPECTJ: AspectJ 패턴 사용
    ex) org.example..Service+ REGEX: 정규 표현식 ex) org.example.Default.
    — CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
    ex) org.example.MyTypeFilter

    1-4 중복 등록과 충돌
    – 자동빈 등록 vs 자동 빈 등록
    — 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록
    — 이름이 같은 경우 스프링은 오류를 발생 (ConflictingBeanDefinitionException)

    – 수동 빈 등록 vs 자동 빈 등록
    — 수동 빈 등록이 우선권을 가짐 ( 수동 빈이 자동 빈을 오버라이딩)


  2. 의존관계 자동 주입
    2-1 다양한 의존 과계 주입 방법
    – 생성자 주입 (권장)
    — 생성자 호출시점에서 딱 1 번만 호출되는 것이 보장
    — 불변, 필수 의존관계에 사용

    – 수정자 주입 (setter 주입)
    — setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입
    — 선택, 변경 가능성이 있는 의존관계에 사용
    — 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용

    – 필드 주입
    — 코드가 간결
    — 외부에서 변경이 불가능해서 테스트 하기 힘들다는 단점
    — DI 프레임워크 없이 불가
    — 사용 자제

    – 일반 메서드 주입
    — 한번에 여러 필드를 주입 가능
    — 일반적으로 사용하지 않음

    2-2 옵션 처리
    – 주입할 스프링 빈이 없어도 동작을 필요로 할 때가 있음
    – @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어 자동 주입 대상이 없으면 오류를 발생
    – 자동 주입 대상을 옵션으로 처리하는 방법
    — @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안 됨
    — org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력
    — Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력

    2-3 생성자 주입 선택 권장 (결론)
    – 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없음
    – 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 됨 (불변)
    – 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 함
    – 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아님
    – 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없음, 따라서 불변하게 설계할 수 있음
    – final 키워드
    — 생성자 주입을 사용하면 필드에 final 키워드 사용 가능
    — 생성자에 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아줌

    2-4 롬복 라이브러리
    – @RequiredArgsConstructor : final이 붙은 필드를 모아서 생성자를 자동으로 만들어 줌 (롬복이 자바의 애노테이션 프로세서라는 기능을 이용)


  3. 빈 생명주기 콜백
    3-1 빈 생명주기 콜백 시작
    데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요

    – 스프링 빈의 라이프사이클 (간략화) : 객체 생성 -> 의존관계 주입
    — 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료
    — 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 함
    — 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공
    — 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 줌
    — 따라서 안전하게 종료 작업 진행 가능

    스프링 빈의 이벤트 라이프사이클
    ( 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료 )
    – 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
    – 소멸전 콜백 : 빈이 소멸되기 직전에 호출
    ( 참고 : 생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다. 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다. )

    – 스프링 빈 생명주기 콜백 방법 
    — 1. 인터페이스 InitializingBean, DisposableBean
    — InitializingBean, DisposableBean를 implements하여 클래스 생성
    — InitializingBean 은 afterPropertiesSet() 메서드로 초기화를 지원
    — DisposableBean 은 destroy() 메서드로 소멸을 지원
    — 초기화, 소멸 인터페이스 단점 : 각 인터페이스는 스프링 전용 인터페이스이므로 해당 코드가 의존 ( 메서드 이름 변경 불가, 내가 코드를 고칠 수 없는 외부 라이브러리에 적용 불가 – 거의 사용하지 않음 )
    — 2. 빈 등록 초기화, 소멸 메서드 지정
    — 설정 정보에 @Bean(initMethod = “init”, destroyMethod = “close”) 처럼 초기화, 소멸 메서드를
    지정 가능
    — 자유로운 메서드 이름 수정
    — 스프링 빈이 스프링 코드에 의존하지 않음
    — 코드가 아니라 설정 정보를 사용하기 떄문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있음
    — 3. 애노테이션 @PostConstruct, @PreDestroy (권장)
    — 최신 스프링에 가장 권장되는 방법
    — 애노테이션 하나만 붙이면 되므로 매우 편리
    — 스프링 종속적 기술이 아닌 JSR-250라는 자바 표준 ( 패키지 javax.annotation.PostConstruct )
    — 단점 : 외부 라이브러리에 적용 불가, 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용

◆ 그 외 사항

포스팅 계획 –  4월 : MVC 모델링, 정처기 실기(+중간) / 5월 : 스프링부트, MVC 모델링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링부트 / 8월 ~ : 알고리즘과 CS, JPA, 스프링부트를 포함한 미정

  • 아직 해당 작성글에 틀린 부분이 많은 것으로 예상됩니다. 언제나 댓글 환영입니다
  • 그리고 이 글은 인프런 김영한 선생님의 Spring 로드맵 과정입니다

 

Spring 입문 3

AppConfig 리팩터링

현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.


기대하는 그림

  • 각 메서드의 반환타입을 구현체가 아닌 인터페이스로 반환 타입으로 반환해야함을 잊지 말자
  • 모든 메서드는 public으로 작성한다.
    AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.

새로운 구조와 할인 정책 적용

처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
FixDiscountPolicy RateDiscountPolicy 어떤 부분만 변경하면 되겠는가?
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다


FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.
AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy -> RateDiscountPolicy
객체로 변경했다.
이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다.
클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.

좋은 객체 지향 설계의 5가지 원칙의 적용 (여기서 3가지 SRP, DIP, OCP 적용)

SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.


클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있음
SRP 단일 책임 원칙을 따르면서 관심사를 분리함
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
클라이언트 객체는 실행하는 책임만 담당


DIP 의존관계 역전 원칙
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.


새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다. 왜냐하면 기존
클라이언트 코드( OrderServiceImpl )는 DIP를 지키며 DiscountPolicy 추상화 인터페이스에
의존하는 것 같았지만, FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했다.
클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경했다.
하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없다.
AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트
코드에 의존관계를 주입했다. 이렇게해서 DIP 원칙을 따르면서 문제도 해결했다.


OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.


다형성 사용하고 클라이언트가 DIP를 지킴
애플리케이션을 사용 영역과 구성 영역으로 나눔
AppConfig가 의존관계를 FixDiscountPolicy RateDiscountPolicy 로 변경해서 클라이언트
코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!

IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.
프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl
도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService
인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다.
이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.


프레임워크 vs 라이브러리


프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit) 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.


의존관계 주입 DI(Dependency Injection)
OrderServiceImpl 은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.


정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는
애플리케이션을 실행하지 않아도 분석할 수 있다. 클래스 다이어그램을 보자
OrderServiceImpl 은 MemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다.
그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl 에 주입 될지 알 수 없다.

동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

객체 다이어그램

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다

스프링으로 전환하기

AppConfig를 스프링 기반으로 변경

  • AppConfig에 설정을 구성한다는 뜻의 @Configuration 을 붙여준다.
  • 각 메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다

MemberApp에 스프링 컨테이너 적용
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
– 작성한 AppConfig의 빈들을 스프링 컨테이너에 등록한 후 생성
MemberService memberService = applicationContext.getBean(“memberService”, MemberService.class);
– 컨테이너에서 등록된 스프링 빈들 중 AppCofing.memberService를 가져옴 (“가져올 메서드 이름”, “반환타입”)

스프링 컨테이너

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )
  • 이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다

컨테이너에 등록된 모든 빈 조회

모든 빈 출력하기

  • 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
  • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
    애플리케이션 빈 출력하기
  • 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력해보자.
  • 스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.
    — ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
    — ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회 대상 스프링 빈이 없으면 예외 발생
NoSuchBeanDefinitionException: No bean named ‘xxxxx’ available
(타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정)

스프링 빈 조회 – 상속 관계
부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.

BeanFactory vs ApplicationContext
BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean() 을 제공한다.
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
    ApplicationContext
  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이가 뭘까?
  • 애플리케이션을 개발할 때는 빈은 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.
  • 정리
  • ApplicationContext는 BeanFactory의 기능을 상속받는다.
  • ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

싱글톤 컨테이너

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
  • 기존에 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다! ->메모리 낭비가 심하다.
    해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글톤 패턴

싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다
참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다

싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
    안티패턴으로 불리기도 한다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다!
    — 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    — 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
    — 가급적 읽기만 가능해야 한다.
    — 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

금주의 IntelliJ 단축키

  • Ctrl + Alt + M : 리팩토링
  • iter : 집합의 각 요소들을 하나씩 꺼내주는 for문 생성
  • soupv : 변수 print 자동 작성
  • Ctrl + D : 드래그 범위 코드를 복사하여 밑에 붙임 (복제)
  • Ctrl + E : 최근 열었던 파일 목록 ( Enter 입력 시, 바로 전 파일 열람)
  • Ctrl + Shift + Enter : 현재 입력바가 어디 있든 다음 라인으로 이동
  • Shift x2 : 메서드, 클래스 등 검색 (Ctrl + N)

◆ 그 외 사항

포스팅 계획 – 3월 : 스프링 / 4월 : 정처기 실기(+중간) / 5월 : 스프링부트, MVC 모델링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링부트 / 8월 ~ : 알고리즘과 CS, JPA, 스프링부트를 포함한 미정

  • 아직 해당 작성글에 틀린 부분이 많은 것으로 예상됩니다. 언제나 댓글 환영입니다
  • 그리고 이 글은 인프런 김영한 선생님의 Spring 로드맵 과정입니다

Spring 입문 2

스프링 입문 공부이지만 스프링을 사용하지 않은 순수 자바로만 구현하는 것을 시작으로 한다.
그 이후 하나씩 스프링을 활용한 코드를 추가하고, 환경이 변경되는 과정을 밟으며 다형성과 DI 등을 공부함에 목적이 있다.

◆ 작업 환경

  • JAVA 11
  • IDE : Intelli J
  • Web Framework : Spring
  • OS : Windows

◆ 프로젝트생성 (환경설정)

  1. start.spring.io에서 Gradle Project, Vers = 안정적인 최신판, Artifact = core, Packaging = Jar, Java = 11 사용
  2. 압축풀고 build.gradle을 open (build 코드 변경 시 reload 필수)
  3. setttings에서 build Tools의 Gradle의 run, test를 IntelliJ로 변경 (직접 실행으로 빠른 속도)

◆ 비즈니스 요구사항과 설계

  • 회원
    회원을 가입하고 조회할 수 있다.
    회원은 일반과 VIP 두 가지 등급이 있다.
    회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    회원은 상품을 주문할 수 있다.
    회원 등급에 따라 할인 정책을 적용할 수 있다.
    할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
    ( 도메인 협력관계, 클래스 다이어그램, 객체 다이어그램 등을 프로그램 설계 시, 필수적으로 작성)

저장소 구현 방법 1. 내부 메모리 사용 (DB의 변경 가능성을 열어둠)

  1. domain 생성
    -1. member 패키지 생성
    -2. Grade를 enum을 생성 (BASIC, VIP)
    -3. Member 엔티티 생성 (Long id, String name, Grade grade)
  2. repository 생성
    -1. repository 패키지 생성
    -2. MemberRepository 인터페이스 생성
    void save(Member), Member findById(Long)
    -3. 상속받아 MemoryMemberRepository 구현체 생성
    private static HashMap store = new HashMap<>();
    (내부 메모리를 사용하므로 HashMap형태의 store 생성 – 동시성 이슈로 인해 concurrent HashMap을 사용해야 하나 테스트용이므로 생략)
    save() : store.put(member.getId(), member)
    findById() : return store.get(memberId)
  3. service 생성
    -1. service 패키지 생성
    -2. MemberService 인터페이스 생성
    void join(Member), Meber findMember(Long)
    -3. 상속받아 MemberServiceImpl 구현체 생성
    MemberRepository Type으로 MemoryMemberRepository 객체 생성 ( m = new)
    join() : m.save()
    findMember() : return m.findById(id)
  4. Member join JUnit Test
    -1. test 파일에 member 패키지 생성
    -2. MemberServiceTest 작성
    new MemberServiceImpl
    @Test
    void join(){
    // given : new Member()
    // when : mService.join() mService.find()
    // then : Assertions.assertThat(member).isEqualTo(findMember) – 두 객체가 같은지 비교
    (Assertions는 org.asserj.core… 을 사용) << 주문도메인 사진>>
  5. 할인 정책 인터페이스와 정액 정책 구현체
    -1. discount 패키지 생성
    -2. DiscountPolicy 인터페이스 작성
    int discount(Member, Int) : return 할인 금액
    -3. 상속받아 FixDiscountPolicy 구현체 작성
    discountFixAmount = 1000 // 반환할 고정 할인 금액
    discount() : if(member.getGrade() == Grade.VIP) {1000} else {0} // enum은 ==을 통해 동일 비교
  6. 주문 엔티티
    -1. order 패키지 생성
    -2. Order 엔티티 작성
    private memberId, itemName, itemPrice, discountPrice
    생성자 + Getter,Setter
  7. 주문 서비스 인터페이스와 구현체
    -1. order 패키지에 OrderService 인터페이스 작성
    Order createOrder(Long memberId, String itemName, int itmePrice)
    -2. 상속받아 OrderServiceImpl 구현체 작성
    미리 만들어둔 memberRepository와 discountPolicy의 구현체를 new로 생성
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
    Member member = memberRepository.findById(memberId); // member 찾기
    int discountPrice = discountPolicy.discount(member,itemPrice); // member 등급에 맞는 할인 금액 반환
    return new Order(memberId,itemName,itemPrice,discountPrice); // 주문 return
    }
    // 참고사항 : 여기서는 discout 정책과 관련되어 구현된 것이 없어, 차후 정책 변경이 유리 (SOLID 중요성)
  8. createOrder Test
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();
    @Test
    void createOrder(){
    //given : new Member(), join() // VIP
    //when : createOrder()
    //then : Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000)
    (VIP로 생성된 Member의 할인 금액이 1000원인지 비교)


객체지향 설계 원칙을 잘 준수했는지 확인하기위해 정액 할인 정책에서 정률 할인 정책으로 변경
(기존의 OrderServiceImpl에서 FixDiscountPolicy를 RateDiscountPolicy로 생성 – 다른 코드는 수정 X)

  1. 정률 할인 정책 구현체 작성
    -1. 기존의 DiscountPolicy 인터페이스를 상속하여 RateDiscountPolicy 생성
    -2. private int discountPercent = 10 : 할인률 10%
    -3. 정액 할인과 똑같이 작성
    if(member.grade==Grade.VIP){} else {}
    -4. Ctrl+Shift+T로 Test Class 생성
  2. Test
    DiscountPolicy discountPolicy = new RateDiscountPolicy();
    @Test // 정상 범위 테스트
    @DisplayName(“VIP는 10% 할인이 적용되어야 한다.”) // Test 결과 출력은 한글로 확인 가능
    void vip_o(){
    //given : new Member
    //when : discount
    //then : Assertions.assertThat(discount).isEqualTo(1000);
    (10% 할인된 금액인 1000원이 맞는지 비교)
    // 참고사항 : Grade를 BASIC으로 설정한 Member가 할인이 적용되지는 않았는지 Test 또한 필요함
  3. 기존 시스템에 할인 정책 교체 적용
    -1. OrderServiceImpl에서 할인 정책을 가져올 때, 단순히 변경
    new FixDiscountPolicy() –> new RateDiscountPolicy();

@ 문제점 발견
우리는 역할과 구현을 충실하게 분리했다. OK
다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK
OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다

  • 그렇게 보이지만 사실은 아니다.

DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를지킨 것 같은데?

  • 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고있다.
    — 추상(인터페이스) 의존: DiscountPolicy
    — 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
  • 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.

왜 클라이언트 코드를 변경해야 할까?
클래스 다이어그램으로 의존관계를 분석해보자.


지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각했다.


잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라
FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다! DIP 위반


중요!: 그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! OCP 위반

어떻게 문제를 해결할 수 있을가?
클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도함께 의존한다.
그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
DIP 위반 추상에만 의존하도록 변경(인터페이스에만 의존)
DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
인터페이스에만 의존하도록 설계를 변경하자


인터페이스에만 의존하도록 코드 변경
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
인터페이스에만 의존하도록 설계와 코드를 변경했다.
그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
실제 실행을 해보면 NPE(null pointer exception)가 발생한다.


해결방안
—이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.—

◆ 관심사의 분리

AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자

  1. Appconfig 작성 (애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는
    별도의 설정 클래스)
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    MemberServiceImpl
    MemoryMemberRepository
    OrderServiceImpl FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
    MemberServiceImpl MemoryMemberRepository
    OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy

설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
단지 MemberRepository 인터페이스만 의존한다.
MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서결정 된다.
MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다


객체의 생성과 연결은 AppConfig 가 담당한다.
DIP 완성: MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

◆ 정리
AppConfig를 통해서 관심사를 확실하게 분리했다.
AppConfig는 공연 기획자다.
AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.

◆ 금주의 IntelliJ 단축키

  • java파일에서 psvm을 치면 자동으로 작성
    public static void main(String[] args) { }
  • sout : System.out.println()
  • Alit + Insert : 생성자와 Getter,Setter는 물론 toString() 또한 자동 생성 가능
    (구현체 자체를 출력 시, 작성된 toString()이 호출하여 print()가능
  • Ctrl + Shift + T : 해단 메서드 Test Class 자동 생성
  • Ctrl + E : 프로젝트 내 파일이나 메서드 검색을 통한 이동

◆ 그 외 사항

포스팅 계획 – 3월 : 스프링 / 4월 : 정처기 실기(+중간) / 5월 : 스프링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링 / 8월 ~ : 알고리즘과 CS, JPA, 스프링부트를 포함한 미정

  • 아직 해당 작성글에 틀린 부분이 많은 것으로 예상됩니다. 언제나 댓글 환영입니다
  • 그리고 이 글은 인프런 김영한 선생님의 Spring 로드맵 과정입니다

Spring 입문 1

◆ 작업 환경

  • JAVA 11
  • IDE : Intelli J
  • Web Framework : Spring
  • OS : Windows

◆ 참고 사항

Controller, Service, Repository 패턴

Controller, Service, Repository 패턴은 정형화 되어있음

Controller = 외부 요청을 받음

Service = 비즈니스 로직을 만듦

Repository = 데이터 저장

Test 시, given, when, then 패턴을 사용하는 것에 익숙해지자

◆ Simple Member Manage System

컴포넌트 스캔, 자동 의존관계 설정 방식 사용

Controller

HomeController

MemberController : 회원 가입 / 조회 기능 제공

MemberForm : 회원 가입 관련 Form 제공

Service

MemberService : 회원 정보 등록 / 조회

Repository

MemberRepository : Interface (회원 등록, 이름으로 조회, ID로 조회, 전체 조회)

MemoryMemberRepository

Domain

Member : 회원 ID, Name 저장

Application

SpringApplication

Templates

home.html

/members/createMemberForm.html : 회원 가입

/members/memberList.html : 회원 조회

TestCase

Repository TestCase

Service TestCase

Application TestCase

◆ 처음부터 다시 만들어본 과정 브리핑

controller, domain, repository, service 디렉토리 만듦

1. Home.html과 HomeController 작성

(@Controller, @GetMapping(“/”), @Autowired)

2. Member domain 작성

– 저장할 회원 정보 private로 멤버 생성 (Getter, Setter)

3. MemberRepository interface 작성

( save, findById, findByName, findAll )

4. 위 interface를 implements한 MemoryMemberRepository 작성

– @Override 자동 생성

– <Long, Member>를 저장할 HashMap type의 store 객체 생성

– Id는 단순 Long ++sequence로 대체

– save(Member) : 받아온 객체에 Id를 ++sequence로 부여, HashMap에 .put( , )

– findById(Id) : return Optional.ofNullable(store.get(id))

– findByName(Name) : return store.values().stream()

.filter(member -> member.getName().equals(name))

.findAny();

– findAll() : return new ArrayList<>(store.values())

– clearStore() : store.clear(); // test를 위한 store 비우기

5. MemberService 작성 (controller에서 repository의 메서드에 직접 접근 할 수 없도록 중간자 역할)

– 생성자로 memberRepository와 연결 (@Autowired)

– findByName()을 통한 중복 회원 검증 메서드 작성 (IllegalStateException)

– join(Member) : 회원가입 (중복 검증 메서드 포함)

– findMembers() : 전체 회원 조회

– findOne(Id) : 해당 Id의 회원 조회

6. MemberController 작성

– 생성자로 memberService와 연결 (@Autowired)

* 회원 가입

– @GetMapping createForm() : 회원가입 html로 이동

– html에서 사용자가 작성한 정보를 받아올 MemberForm 작성 (private String name + Getter/Setter)

– @PostMapping create(Member) : new Member – setName – join – “redirect:/”(Home 화면으로)

* 회원 조회

– @GetMapping list(Model) : service의 findMembers()를 통해 repository의 전체 회원 조회

– service의 findMembers()를 통해 List<Member>를 받아옴

– model에 addAttribute( , )를 통해 받아온 list<>를 추가

– thymeleaf의 th:each문 : for문과 유사하게 받아온 model의 list 인덱스를 반복

– 회원을 조회할 수 있는 html문 작성

◆ 이번주 IntelliJ 단축키

– Extract Variable = Crtl + Alt + V (저장 객체 자동 작성)

– Extract Method = Crtl + Alt + M (드래그를 범위 method로 작성)

– Declaration or Usage = Crtl + B (정의된 method로 이동)

– Creat Test = Ctrl + Shift + T (Test Case 자동 생성)

– 주석 처리 = Ctrl + /

– ReRun = Shift + F10

– Constructor = Alt + Insert (생성자 등 여러가지 자동 생성)

◆ 그 외 사항

포스팅 계획 – 3월 : 스프링 / 4월 : 정처기 실기(+중간) / 5월 : 스프링 / 6월 : 졸업 작품(+기말) / 7월 : 스프링 / 8월 ~ : 알고리즘과 CS를 포함한 미정

Effective Java – Consider a builder when faced with many constructor parameters

정적 팩토리와 생성자에는 동일한 제약 조건이 있다. -> 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.

매개 변수가 많아질 경우 사용할 수 있는 세 가지를 고려해 볼 수 있다.

  • 텔레스코핑 생성자 패턴
  • 자바빈즈 패턴
  • 빌더 패턴

(1) Telescoping constructor pattern

필수 매개변수와 선택 매개변수를 갖는 생성자의 형태를 띤다. 아래에 예시를 나타내겠다.

필수 매개변수만 갖는 생성자
필수 매개변수 + 하나의 선택 매개변수 생성자
필수 매개변수 + 두 개의 선택 매개변수 생성자
...
...

위와 같이 필수 매개변수만 갖는 생성자를 생성할 수 있고, n개의 선택 매개변수를 생성하는 생성자를 함께 갖는 경우의 방식이다.

예제) 식품의 영양 정보를 표현하는 클래스를 생각해보자, 식품의 양, 개수, 칼로리, 총 지방, 포화 지방 등 20개 이상의 선택 필드를 가질 수 있다. 몇 가지만 값을 가지고 대부분은 0의 값을 가진다. 생성자를 어떻게 만들까?

public class NutritionFacts{
  private final int servingSize; //required
  private final int servings; //required
  private final int calories; //optional
  private final int fat; //optional
  private final int sodium; //optional
  private final int carbohydrate; //optional

public NutritionFacts(int servingSize, int servings){
  this(servingSize, servings, 0);
}

public NutritionFacts(int servingSize, int servings, int calories){
  this(servingSize, servings, calories, 0);
}

public NutritionFacts(int servingSize, int servings, int calories, int fat){
  this(servingSize, servings, calories, fat, 0);
}

public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium){
  this(servingSize, servings, calories, fat, sodium, 0);
}

public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate){
  this.servingSize = servingSize;
  this.servings = servings;
  this.calories = calories;
  this.fat = fat;
  this.sodium = sodium;
  this.carbohydrate = carbohydrate;
}
}

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

위와 같이 오버로딩을 사용하여 나타내는 방식이 점층적 생성자 패턴이라고 한다.

대신 위와 같은 방법을 사용하면 원하지 않은 변수에도 초기값을 설정해야 한다.

오버 로딩 -> 같은 이름이지만 변수의 개수 또는 타입이 달라 여러 개를 정의할 수 있다. 

단점

위처럼 점층적 생성자 패턴도 쓸 수 있지만, 매개변수의 개수가 많아지면 클라이언트 코드 작성이 힘들고 가독성이 떨어진다.

저 생성자 코드를 보면 내가 무엇에게 값을 주는지 알기 어렵고 어떤 파라미터에 값을 입력하는지 주의해서 봐야 한다.

또한, 동일한 타입의 매개변수가 늘어져 있다면 찾기 어려운 버그로 이어질 수 있다. 

클라이언트가 실수해서 다른 생성자를 선택하거나, 알아채지 못할 수 있다.

(2) JavaBeans pattern

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 이용하여 원하는 값을 설정하는 방식

public class NutritionFacts{
  private int servingSize = -1; //required
  private int servings = -1; //required
  private int calories = 0; //optional
  private int fat = 0; //optional
  private int sodium = 0; //optional
  private int carbohydrate = 0; //optional

  public NutritionFacts(){}

  public void setservingSize (int val) { servingSize = val };
  public void setservings (int val) { servings = val };
  public void setcalories (int val) { calories = val };
  public void setfat (int val) { fat = val };
  public void setsodium (int val) { sodium = val };
  public void setcarbohydrate (int val) { carbohydrate = val };

}

위처럼 아까 Telescoping constructor pattern 보다는 생성자 코드가 길지만 보기는 쉽게 변한다는 장점이 있다.

어떻게 매개변수에 setter  메서드가 존재하기에

cocaCola.setServingSize(240); 과 같이 값을 지정해주면 된다.

단점

1. 여러 메소드 호출로 나누어져 인스턴스가 생성되므로, 생성 과정이 이뤄지는 동안 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓기에 된다.

2. 클래스를 불변으로 만들 수 없어서 프로그래머가 추가 작업을 해주어야 한다.

(3) Builder pattern

-> 점층적 생성자 패턴의 안전성 + 자바 빈즈의 가독성

장점

1. 작성이 쉽고 가독성이 좋다. -> 롬복을 이용하면 더 쉽게 된다.

2. 불변 규칙을 이용할 수 있고, 검사 또한 가능하다. -> IllegalStateException을 통해 예외 처리 가능하다.

단점

1. 어떤 객체를 생성하기 위해 빌더를 만들어야 가능하기에 성능상 문제가 될 수 있다.

2. 매개 변수가 4개 이상이 될 경우 사용하는 것이 좋다. Telescoping 보다 더 긴 코드가 생성될 수 있다.

-> 그래도 Builder pattern을 염두에 두고 코드를 구현해라.

public class NutritionFacts {
	private final int servingSize;
	private final int servings;
	private final int calories;
	private final int fat;
	private final int sodium;
	private final int carbohydrate;

	public static class Builder {
		private final int servingSize;  // 필수
		private final int servings;     // 필수
		private int calories = 0;
		private int fat = 0;
		private int sodium = 0;
		private int carbohydrate = 0;

		public Builder(int servingSize, int servings) {
			this,servingSize = serginsSize;
			this.servings = servings;
		}

		public Builder fat(int val) {
			fat = val;
			return this;
		}

		public Builder sodium(int val) {
			sodium = val;
			return this;
		}

		public Builder carbohydrate(int val) {
			carbohydrate = val;
			return this;
		}

		public NutritionFacts build() {
			return new NutritionFacts(this);
		}
	}

	private NutirionFacts(Builder builder) {
		servingSize = builder.servingSize;
		servings = builder.servings;
		calories = builder.calories;
		fat = builder.fat;
		sodium = builder.fat;
		carbohydrate = builder.carbohydrate;
	}
}

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
	.calories(100)
	.sodium(35)
	.carbohydrate(27)
	.build();

정리

생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바 빈즈보다 훨씬 안전하다.

Effective Java – static factory method

클래스의 인스턴스를 얻는 전통 수단은 public 생성자이다.

하지만, 꼭 알아둬야 하는 기법 -> static factory method

public class item1 {

  private String name;

  // public 생성자
  public item1(String name) {
    this.name = name;
  }

  // static factory method
  public static item1 myName(String name) {
    return new item1(name);
  }

  public static void main(String[] args) {
    item1 my = myName("haessae0"); // static factory method
    item1 my2 = new item1("haessae0"); // public 생성자
  }
}

이렇게 클래스는 정적 팩토리 소드를 제공할 수 있다.

장점

– 이름을 가질 수 있다.

– 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

– 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

– 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

– 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

단점

– 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가

– 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.


장점

1. 이름을 가질 수 있다.

정적 팩토리 메소드는 이름만 잘 지으면 반환되는 객체의 특성을 명시적으로 표현할 수 있다.

public class item1 {

  private String name;

  // public 생성자
  public item1(String name) {
    this.name = name;
  }

  // static factory method
  public static item1 myName(String name) {
    return new item1(name);
  }

  public static void main(String[] args) {
    item1 my = myName("haessae0"); // static factory method
    item1 my2 = new item1("haessae0"); // public 생성자
  }
}

앞서 작성한 코드를 보자면

1.

public 생성자로 만든 것 -> 호출하였을 때, ‘haessae0’라는 것이 어떤 인스턴스 변수인지 알기 어렵다. -> 클래스 이름인 ‘item1’에게 전달하여 생성하는 것이기 때문이다.

2. 

정적 팩토리 메소드로 만든 것 -> 호출하였을 때, ‘haessae0’라는 것이 myName이라는 것을 알 수 있다. -> 내 이름이 haessae0라는 것을 알 수 있다.

당연히 알기 쉬운 정적 팩토리 메서드를 사용할 것이다.

또한, 

public 생성자는 하나의 시그니처로 하나만 생성할 수 있다.

public class item1 {

  private String name;
  private String birth;

  // public 생성자
  public item1(String name) {
    this.name = name;
  }

  // public 생성자 -> 불가
  public item1(String birth) {
    this.birth = birth;
  }

}

이미 name 변수를 받는 생성자가 존재 하기 때문에 birth를 받는 생성자는 생성할 경우 오류를 범하고 만다.

왜? 이미 하나의 변수를 받는 생성자가 존재하기 때문에 사용을 하여도 엉뚱한 메소드를 호출할 수 있다.

하지만! 정적 팩토리 메서드는 가능하게 해준다.

public class item1 {

  private String name;
  private String birth;

  // static factory method
  public static item1 myName(String name) {
    item1 my = new item1();
    my.name = name;
    return my;
  }

  // static factory method
  public static item1 myBirth(String birth) {
    item1 my = new item1();
    my.birth = birth;
    return my;
  }

}

이렇듯이 하나의 시그니처로 여러 가지를 구현할 수 있다.


2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

불변 클래스는 인스턴스를 미리 만들거나 캐싱하여 재활용하는 방식이기에 불필요한 객체 생성을 막을 수 있다.

대표적으로 Boolean.valueOf(boolean) 메서드가 있다.

public final class Boolean implements java.io.Serializable,Comparable<Boolean> {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
}

상수 객체로 선언해주기 때문에 새로운 객체를 매번 만들지 않는다.

-> 생성 비용이 큰 객체가 자주 불려지는 상황에 정적 팩토리 메서드를 사용하면 그 성능을 많이 이끌어 낼 수 있다.

또한, 반복되는 객체 요청을 언제 어느 순간에 인스턴스를 살고 죽게 할지 통제하는 인스턴스 통제 컨트롤이 있다.

왜? 사용할까?

– 인스턴스를 통제하면 싱글톤이나 인스턴스화 불가 상태로 만들 수 있다.

– 불변 값 클래스에서 동치인 인스턴스가 하나인 것을 보장 -> a == b일 때만, a.equals(b)가 성립된다.

– 플라이 웨이트 패턴의 근간이고, 열거 타입은 인스턴스가 하나만 만들어지는 것을 보장한다.


3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

코드의 유연성을 제공해준다. -> 인터페이스를 정적 팩토리 메서드 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.

동반 클래스에서 주로 사용된다.

자바 1.8 이전 -> 인터페이스에 정적 메서드를 선언할 수 없었다. 따라서 인터페이스에 기능을 추가하기 위해서는 동반 클래스라는 것을 만들어 그 안에 정적 메서드를 추가했다.

-> 굳이 별도의 문서를 찾아가며 수현 클래스가 무엇인지 알아보지 않아도 된다.


4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다른 클래스가 객체를 반환해도 된다.

예시로 EnumSet 클래스가 존재한다.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

클래스에는 noneOf라는 메서드가 존재하는데 잘 보면 universe의 개수가 64개 이하면 RegularEnumset의 인스턴스를 반환하고, 65개 이상이면 JumboEnumset의 인스턴스를 반환한다.

두 타입 모두 알려지지 않기 때문에 클라이언트는 인지하지 않아도 되며 나중에 삭제하거나 새로운 타입을 만들어도 문제없이 사용할 수 있다. 그저 Enumset의 하위 클래스에만 존재하면 된다.


5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이러한 유연함은 서비스 제공자 프레임워크를 만드는 기반이 된다. -> 대표 JDBC

서비스 제공자 프레임워크의 구성요소

  • Service Interface : 구현체의 동작 정의
    • Connection
  • Provider Registration API : 제공자가 구현체를 등록할 때 사용
    • DriverManager.registerDriver
  • Service Access API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
    • DriverManager.getConnection
  • Service Provider Interface : 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체
    • Driver

단점

– 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가

– 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가

java.util.Collections로 만든 구현체는 상속할 수 없다.

2. 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

Javadoc 문서에서 따로 정리하지 않는다. API 문서를 잘 써놓고 메소드 이름도 알려진 규약을 따라 짓는 식으로 문제를 해결해줘야 한다.


명명 방식

from매개 변수를 하나 받아 해당 타입의 인스턴스를 반환하는
형변환 메소드
Date d = Date.from(instant);
of여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는
집계 메소드
Set<Rank> faceCards =
EnumSet.of(JACK, QUEEN, KING);
valueOfFrom과 of의 더 자세한 버전BigInteger prime =
BigInteger.valueOf(Integer.MAX_VALUE);
instance
getInstance
매개변수로 명시한 인스턴스를 반환하지만, 
같은 인스턴스임을 보장하지 않는다.
StackWalker luke =
StackWalker.getInstance(options);
create
newInstance
instance & getInstance와 같지만, 매번 새로운 인스턴스
생성해 반환을 보장
Object newArray = 
Array.newInstance(classObject, arrayLen);
getTypegetInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.
“Type”은 팩토리 메소드가 반환할 객체의 타입
FileStroe fs = Files.getFileStore(path);
newTypenewInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.
“Type”은 팩토리 메소드가 반환할 객체의 타입
BufferedReader br =
Files.newBufferedReader(path);
typegetType과 newType의 간결한 버전List<Complaint> litany =
Collections.list(legacyLitany);

핵심

정적 팩토리 메서드와 생성자는 각자의 쓰임새가 있어 상대적인 장단점 이해 후 사용
그래도 정적 팩토리 메서드가 훨씬 좋다.