게시글 목록을 조회할 때 페이지 번호로 목록이 구분되는 걸 많이 본적이 있을 거다.

한 화면에 모든 글을 보여줄 수 없기 때문에 페이지로 나눠 게시글 목록을 보여주는 것인데 오늘은 이 페이징 처리에 관해 글을 써보려고 한다.

 

이 기능을 실제로 하나하나 구현할 시 상당히 번거로운데 Spring Data JPA에서는 Pageable을 이용해 손쉽게 페이징 처리를 할 수 있도록 도와준다. 이떄 단순하게 pagination만 해주는 게 아니라 보여지는 목록에 정렬이 필요한 경우 sorting도 같이 할 수 있다.

 

1. 페이징 처리할 레포지토리에 JPARepositoy를 상속한다.

@Repository
public interface postRepository extends JpaRepository<Post, Long> {
	List<Post> findAll(Pageable pageable);
}

2. 페이징 처리

pageable 객체를 만들어 repository의 메소드에 인자로 전달하면 된다.

Pageable pageable = PageRequest.of(현재 페이지 넘버, 조회할 페이지의 수, 정렬관련정보들);

2-1. 페이징 처리

Pageable pageable = PageRequest.of(page - 1, 10);
repo.메소드명(pageable);

페이지 번호는 0번 부터 시작해 1을 빼줘야한다.

 

2-2. 페이징 처리+정렬

Pageable pageable = PageRequest.of(0, 3, Sort.by("postId").descending());
repo.메소드명(pageable);

 

 

참고자료

 

https://www.baeldung.com/spring-data-jpa-pagination-sorting

https://velog.io/@jjun_meatlov/Spring-Data-JPA-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

https://devlog-wjdrbs96.tistory.com/414

http://devstory.ibksplatform.com/2020/03/spring-boot-jpa-pageable.html

 

토비의 스프링 3.1 책을 읽으며 일부분을 정리한 내용입니다. 
개념 이해를 위한 내용들 중 일부분을 정리했으므로 이해를 돕기위한 예시 코드들 및 자세한 전체적인 내용은 책을 통해 확인하시길 바랍니다.
포스팅 내용이 저작권의 문제가 발생할 경우 게시물은 바로 삭제/비공개 처리됩니다.

트랜잭션 속성

public Object invoke(MethodInvocation invocation) throws Throwable {
    TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    try {
        Object ret = invocation.proceed();
        this.transactionManager.commit(status);
        return ret;
    } catch(RuntimeException e) {
        this.transactionManager.rollback(status);
        throw e;
    }
}

 

DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는

트랜잭션의 동작방식에 영향을 줄 수 있는 4가지 속성 정의

1. 트랜잭션 전파

트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것 인가를 결정하는 방식

 

PROPAGATION_REQUIRED

진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다.

RPOPAGATION_REQUIRES_NEW

항상 새로운 트랜잭션을 시작

PROPAGATION_NOT_SUPPORTED

진행 중인 트랜잭션이 있어도 무시

->여러 메소드 동작시 특정 메소드만 트랜잭션 적용을 제외시키기 위해 사용

 

2. 격리 수준

서버환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있으므로 DB 트랜잭션은 격리수준을 갖고 있어야 한다. 

적절한 격리수준을 조정해서 가능한 여러 트랜잭션을 동시에 진행하면서 문제가 발생하지 않는 제어가 필요
기본적으로 DB에 설정되어 있지만 DataSource 등에서 재설정 가능

DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT
->DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻

3. 제한 시간

트랜잭션을 수행하는 제한 시간

DefaultTransactionDefinition의 기본 설정은 제한시간 x

제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED나 RPOPAGATION_REQUIRES_NEW와 함께 사용해야만 유의미

4. 읽기 전용

트랜잭션 내에서 데이터 조작하는 시도 방지

 

 

TransactionInterceptor

스프링에서 제공하는 트랜잭션 경계설정 어드바이스

프로퍼티

PlatformTransactionManager

Properties 타입의 transactionAttributes

네 가지 기본 항목을 정의한 TransactionDefinition 인터페이스

+ rollbackOn() 메서드를 가지고 있는TransactionAttribute 인터페이스

rollbackOn() 메서드는 롤백을 수행할 예외상황을 결정하는 메서드

메서드 패턴을 키로, 트랜잭션 속성을 값으로 

 

메서드 패턴

  • ROPAGATION_NAME : 필수항목, PROPAGATION_ 으로 시작
  • ISOLATION_NAME : 생략가능, ISOLATION_으로 시작
  • readOnly : 생략가능, 읽기전용
  • timeout_NNNN : 생략가능, 제한시간 지정. NNNN은 초단위로 지정
  • -Exception1 : 한 개 이상 등록가능, 체크 예외중 롤백 대상으로 추가할 것
  • +Exception2 : 한 개 이상 등록가능, 런타임이지만 롤백되지 않을 예외 지정

트랜잭션 속성 정의 예시

    <bean id="transactionAdvice" 
              class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly,timeout_30</prop>
                <prop key="upgrade*">PROPAGATION_REQUIRES_NEW,ISOLATION_SERIALIZABLE</prop>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

TransactionInterceptor는 2가지 예외 처리 방식 제공

1. 런타임 예외의 경우 트랜잭션은 롤백

2. 체크 예외를 던지는 경우 비즈니스 로직에 따라 개발자가 의도한 예외라 해석하고 트랜잭션을 커밋

 

위 2가지 상황에 부합하지 않는 예외는 rollbackOn() 메서드 활용

 

tx 네임스페이스

TransactionInterceptor 타입의 어드바이스 빈과 TransactionAttribute 타입의 속성 정보도 tx 스키마의 전용 태그를 이용해 정의 가능

 

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            https://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/aop
                            http://www.springframework.org/schema/aop/spring-aop.xsd
                            http://www.springframework.org/schema/tx
                            http://www.springframework.org/schema/tx/spring-tx.xsd">

         ...
    <tx:advice id="transactionAdvice", transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" /*속성들*/ />
                        <tx:method name="upgrade*" propagation="..." isolation="SERIALIZABLE"/>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

 

포인트 컷과 트랜잭션 속성의 적용 전략

  1. 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용
    • 트랜잭션을 적용할 타깃 클래스의 메서드는 모두 트랜잭션 적용 후보
    • 읽기 전용 메서드도 트랜잭션 적용
    • execution() 방식의 포인트컷 대신 bean() 표현식을 사용하는 것도 고려
  2. 공통된 메서드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의
    • get, find와 같이 조회전용 메서드의 접두어를 정해두기
  3. 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메서드를 호출할 때는 적용되지 않는다.

 

트랜잭션 속성 적용

1. 트랜잭션 경계설정의 일원화

트랜잭션 경계설정 부가기능을 다양한 계층에서 중구난방으로 적용하는 것은 좋지 않다.

일반적으로 서비스 계층 오브젝트 메서드가 가장 적절

서비스 계층을 트랜잭션 경계로 정했다면, DAO에 직접 접근은 지양

 

2. 서비스 빈에 적용되는 포인트컷 표현식 등록

포인트컷 표현식을 비즈니스 로직의 서비스 빈에 적용되도록 작성

 

3.트랜잭션 속성을 가진 트랜잭션 어드바이스 등록

TransactionInterceptor를 tx:advice 태그를 이용해 등록하고, attributes를 잘 정의

스프링 부트 data jpa를 활용해 게시물 단건 조회 API를 만들던 중 발생한 에러다.

에러 메세지를 살펴보니 무한 재귀 문제임을 알 수 있었다.

양방향 다대다 관계를 일대다 다대일로 찢어서 연관관계 매핑을 한 칼럼이 있었는데 해당 객체를 response로 반환할 시

서로를 참조하며 무한 재귀 함수가 실행되는 문제였다.

 

@ManyToOne 칼럼에 @JsonIgnore 애노테이션을 추가해서 해결했다.

예시 코드는 아래와 같다.

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_idx")
    private Board board;

   
    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stack_idx")
    private TechStack techStack;

 

참고사이트 

https://thalals.tistory.com/227

https://cupeanimus.tistory.com/57

 

계속 널값이 뜬다고 오류가 뜨길래 코드에 문제가 있어서 정보를 못 읽어오는 줄 알고 삽질한 오류이다.

이후에 다시 생각해보니 객체 생성관련 문제인 것 같았다.

 

이전에 jdbc를 사용할 때와 달리 jpa는 객체 단위로 데이터를 처리한다.

해당 DTO 클래스에서 다른 객체의 서비스단,레포지토리단 코드를 활용해 다른 객체의 정보를 가져오는 코드들이 있었다.

생각해보니 다른 객체는 생성이 안된 상태이므로 객체 자체가 null이라 널포인터 에러가 뜨는 것 같았다. 그래서 DTO 클래스에 직접 해당 객체들을 주입해서 디버깅을 하려고 했는데 그러면 더이상 DTO가 아니라 서비스 클래스가 되는 문제가 생겼다.

어떻게 하면 좋을까 생각하다가 문제가 되는 다른 서비스단이나 레포지토리를 사용하는 코드들을 다 서비스단으로 옮겼다.

그리고 서비스에서 해당 코드를 통해 받은 값들을 파라미터에 넣어 DTO 생성자를 통해 값을 넣어줬다.

이 방식을 통해 에러를 디버깅할 수 있었다.

 

관련된 내가 작성한 코드는 아래와 같다.

DTO

package backend.whereIsMyTeam.board.dto;

import backend.whereIsMyTeam.board.domain.*;
import backend.whereIsMyTeam.board.repository.CommentRepository;
import backend.whereIsMyTeam.board.repository.PostLikeCustomRepository;
import backend.whereIsMyTeam.board.repository.PostLikeCustomRepositoryImpl;
import backend.whereIsMyTeam.board.service.PostLikeService;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GetBoardResponseDto {

    private Long boardIdx;
    private String category; //프로젝트, 스터디, 대회 중 1
   // @JsonFormat
    List<TechStackBoard> stackList= new ArrayList<>();; //모집하는 스택 기술들 리스트
    private String title;
    private postDetailDto detail;
    private String postText;
    private List<BoardStatus> boardStatus=new ArrayList<>(); //게시물 상태(삭제,임시저장,모집중,모집완료)
    private postUserDto writer; //게시물 작성자 정보
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime createdAt;
    private Long watch;
    private Long heart; //찜 총 수
    private String isHeart; //해당 유저가 찜 눌렀는지 안눌렀는지
    private List<postCommentDto> commentList= new ArrayList<>();; //댓글 부분 작성자 인덱스와 comment 인덱스도


    public GetBoardResponseDto(Board post,Long heart, String isHeart,List<postCommentDto> c){
        this.boardIdx=post.getBoardIdx();
        this.category=post.getCategory().getCategoryName();
        this.title=post.getTitle();
        this.detail=new postDetailDto(post);
        this.postText=post.getContent();
        this.boardStatus=post.getBoardStatuses();
        this.writer= new postUserDto(post.getWriter()); //userImg 구현중
        this.createdAt=post.getCreateAt();
        this.watch=post.getCnt();
        this.heart=heart;
        this.isHeart=isHeart;
        this.commentList=c;
    }

    public GetBoardResponseDto(Board post,Long heart,List<postCommentDto> c){
        this.boardIdx=post.getBoardIdx();
        this.category=post.getCategory().getCategoryName();
        this.title=post.getTitle();
        this.detail=new postDetailDto(post);
        this.postText=post.getContent();
        this.boardStatus=post.getBoardStatuses();
        this.writer= new postUserDto(post.getWriter()); //userImg 구현중
        this.createdAt=post.getCreateAt();
        this.watch=post.getCnt();
        this.heart=heart;
        this.isHeart="not exist";
        this.commentList=c;
    }


}

 

/**
     * 게시물 단건 조회
     * @return GetBoardResponseDto
     */
    @Transactional
    public GetBoardResponseDto boardDetail(Long boardIdx,Long userIdx) {
        Optional<Board> optional = boardRepository.findByBoardIdx(boardIdx);
        if(optional.isPresent()) {
            Board board = optional.get();
            //방문자 수 1 증가
            board.setHitCnt(board.getHitCnt() + 1);
            boardRepository.save(board);
            //조회 로직 회원,비회원 구분 해야함
            if(userIdx!=0) { //회원
                long heart=postLikeRepository.findPostLikeNum(boardIdx);
                String isHeart=postLikeService.checkPushedLikeString(userIdx,boardIdx);
                List<postCommentDto> commentList=postCommentDto.toDtoList(commentRepository.findAllWithUserAndParentByBoardIdxOrderByParentIdxAscNullsFirstCommentIdxAsc(boardIdx));
                return new GetBoardResponseDto(boardRepository.findByBoardIdx(boardIdx).orElseThrow(BoardNotExistException::new),heart,isHeart,commentList);
            }
            else{ //비회원
                long heart=postLikeRepository.findPostLikeNum(boardIdx);
                List<postCommentDto> commentList=postCommentDto.toDtoList(commentRepository.findAllWithUserAndParentByBoardIdxOrderByParentIdxAscNullsFirstCommentIdxAsc(boardIdx));

                return new GetBoardResponseDto(boardRepository.findByBoardIdx(boardIdx).orElseThrow(BoardNotExistException::new),heart,commentList);
            }

        }
        else {
            throw new NullPointerException();
        }
    }
토비의 스프링 3.1 책을 읽으며 일부분을 정리한 내용입니다. 
개념 이해를 위한 내용들 중 일부분을 정리했으므로 이해를 돕기위한 예시 코드들 및 자세한 전체적인 내용은 책을 통해 확인하시길 바랍니다.
포스팅 내용이 저작권의 문제가 발생할 경우 게시물은 바로 삭제/비공개 처리됩니다.

템플릿/콜백 패턴

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

템플릿: 전략패턴의 컨텍스트, 고정된 흐름의 코드 재사용

콜백: 익명 내부 클래스로 만들어지는 오브젝트, 템플릿 안에서 호출되는 것을 목적으로 만들어진다.

 

템플릿

어떤 목적을 위해 미리 만들어둔 모양이 있는 틀

콜백

실행을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트

파라미터로 전달되지만 값 참조용이 아니라 메소드를 실행시키기 위해 사용

왜냐, 자바에선 메소드 자체를 파리미터로 전달하지 못하기 때문에 메소드가 담긴 오브젝트를 전달

 

템플릿/콜백의 특징

일반적인 DI 방식: 템플릿에 인스턴스 변수 만든  후 수정자 메소드로 의존 오브젝트 받아서 사용

템플릿/콜백 DI 방식: 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달 받는다.

-보통 단일 메소드 인터페이스 사용

-콜백: 단일 메소드 인터페이스를 구현한 익명 내부 클래스로 만들어진다.

-전략 패턴, DI의 장점, 익명 내부 클래스 사용 전략의 결합

 

템플릿/콜백 작업 흐름

책 p.242

클라이언트 역할: 템플릿 안에서 실행될 로직 담은 콜백 오브젝트 만들기, 콜백이 참조할 정보 제공, 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달(메소드 레벨  DI)

템플릿 역할: 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드 호출, 콜백이 돌려준 정보를 사용해서 작업을 마저 수행, 경우에 따라 결과 클라이언트에게 반환

 

콜백 역할: 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행, 그 결과를 템플릿에 반환

 

템플릿/콜백 방식의 단점

DAO메소드에서 매번 익명 내부 클래스를 이용하는데 익명 내부 클래스는 상대적으로 작성/해석에 용이하지 않다.

->복잡한 익명 내부 클래스의 사용을 최소화할 수 없을까?

 

콜백의 분리와 재활용, 템플릿과의 결합

재사용이 가능한 코드를 분리를 해내 익명 내부 클래스 코드를 간결하게 만들면 된다.

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

책 p.247 그림
public class JdbcContext {
...
	public void executeSql(final String query) throws SQLException{
		workWithStatementStrategy(
				new StatementStrategy() {
					public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
						return c.prepareStatement(query);
					}
				}
		);
	}
public void deleteAll() throws SQLException {
	this.jdbcContext.executeSql("delete from users");
}

하나의 목적을 위해 긴밀한 관계를 형성하고 있는 코드들은 응집력이 강하기 때문에 위  JdbcContext 예시처럼 한 곳에 작성하는 게 좋다.

 

템플릿/콜백의 응용법

고정된 로직이 자주 반복되는 코드를 가지고 있다면 이를 분리할 방법을 생각해보자.

중복된 코드는 먼저 메소드로 분리하는 시도

그중 일부 작업이 필요에 따라 바뀐다면 인터페이스를 사이에 두고 분리해서 전략 패턴 적용/DI 의존관계 관리

바뀌는 부분이 한 어플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴 적용 고려

 

템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 전달하는 정보가 무엇인지 판단하는 게 가장 중요

 

제네릭스를 이용한 콜백 인터페이스

제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메소드 정의 가능

제네릭 타입 파라미터  T

 

 

토비의 스프링 3.1 책을 읽으며 일부분을 정리한 내용입니다. 
개념 이해를 위한 내용들 중 일부분을 정리했으므로 이해를 돕기위한 예시 코드들 및 자세한 전체적인 내용은 책을 통해 확인하시길 바랍니다.
포스팅 내용이 저작권의 문제가 발생할 경우 게시물은 바로 삭제/비공개 처리됩니다.

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

-복잡한 블록이 2중으로 중첩까지 되어 있어 조금이라도 코드에 실수가 있을 시 리소스가 부족한 심각한 문제가 발생할 가능성이  높다.

-풀이 가득차버려(리소스가 부족하여) 잘못된 부분을 찾아나가는 과정에서도 코드의 양의 광범위해 찾기가 쉽지않다.

 

이러한 문제점들 해결하려면?

많은 곳에서 중복되지만 변하지 않는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 분리해내는 작업 필요

 

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

-메소드 추출 리팩토링

변하는 부분을 메소드로 추출

public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		
		try {
			c = dataSource.getConnection();
			ps = makeStatement(c);
			ps.execute();
		} catch (Exception e) {
			throw e;
		}finally {
			if(ps != null) { try { ps.close(); } catch (SQLException e) {} }
			if (c != null) { try { c.close(); } catch (SQLException e) {} }
		}
	}
    
	private PreparedStatement makeStatement(Connection c) throws SQLException {
		PreparedStatement ps;
		ps = c.prepareStatement("delete from users");
		return ps;
	}

문제점

보통 분리시킨 메소드를 다른 곳에서 재사용하는데 위와 같은 방식은 재사용이 불가능하다.

변하지 않는 부분을 메소드로 추출하는 것은 try/catch/finally 구조상 쉽지않은 상황

 

-템플릿 메소드 패턴의 적용

템플릿 메소드 패턴

:상속을 통해 기능을 확장해서 사용

변하지 않는 부분은 슈퍼클래스에 변하는 부분은 추상 메소드로 정의 후 서브클래스에서 오버라이드하여 정의 후 사용

 

변하지 않는  try/catch/finally 블록 슈퍼클래스와 상속을 통해 바꿀 수 있는 부분은(위의 예시 PreparedStatement 부분) 서브클래스로 분리

문제점

-다중 상속이 되지 않기 때문에 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다.

-확장구조가 이미 클래스를 설계 시점에서 고정된다.

 

-전략패턴의 적용

오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인퍼페이스를 통해서만 의존하도록 만드는 패턴

개방 폐쇄 원칙에 잘 부합하고 유연하면서 확장성이 좋다.

확장이 되는 부분이 변화되는 부분으로 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임

public interface StatementStrategy {
	PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
public class DeleteAllStatement implements StatementStrategy {
	public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
		PreparedStatement ps = c.prepareStatement("delete from users");
		return ps;
	}
}
	public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		
		try {
			c = dataSource.getConnection();
			StatementStrategy strategy = new DeleteAllStatement();
			ps = strategy.makePreparedStatement(c);
			ps.execute();
		} catch (Exception e) {
			throw e;
		}finally {
			if(ps != null) { try { ps.close(); } catch (SQLException e) {} }
			if (c != null) { try { c.close(); } catch (SQLException e) {} }
		}
	}

문제점

StatementStrategy strategy = new DeleteAllStatement(); 코드를 보면

인터페이스뿐만 아니라 특정 클래스까지 알고있으므로 부적합

 

-DI 적용을 위한 클라이언트/컨텍스트 분리

어떤 전략을 사용할지 클라이언트에게 결정권을 준다.

클라이언트가 전략 하나를 선택하고 오브젝트로 만들어서 컨텍스트에 전달

컨텍스트는 전달받은 전략 구현 클래스의 오브젝트 사용

 

클라이언트 역할을 할  deleteAll 메소드 예시 코드

public void deleteAll() throws SQLException {
		StatementStrategy st = new DeleteAllStatement();
		jdbcContextWithStatementStrategy(st);
}

DI의 중요한 개념은 제3자의 도움을 통해 두 오즈젝트 사이의 유연한 관계가 설정되는 것!

+ Recent posts