no image
[Java] 6자리 인증번호 생성
사용자 기능 중 회원가입, 비밀번호 찾기 등에서 휴대폰 본인인증 과정에서 생성되는 6자리 인증번호를 만들어보자. 1. Random 객체를 통해 난수 추출Random random = new Random(); // 랜덤 객체int randomNum = random.nextInt(9) // 0~9 까지의 난수 1개 생성해당 객체를 통해 0에서 9까지의 한자리 숫자를 추출할 수 있다.추출되는 숫자는 정수형이다.   2. String 문자열 연산을 통해 한가지 씩 추출되는 난수를 결합String authNum = ""; // 빈 문자열 생성 authNum += Integer.toString(randomNum); // 생성된 난수를 String 타입으로 변환 후 문자열 연산앞서 생성한 I..
2024.09.22
[Spring] 스프링 시큐리티 PasswordEncoder 간단하게 사용하기
Spring 프로젝트를 개발하고 있는데,인증 기능에 스프링 시큐리티 전체를 사용하지 않는다. 그래서 인증 기능을 따로 구현하게 됐는데하지만, 비밀번호는 암호화를 해야하기 때문에 스프링 시큐리티에게 인증 제어를 받지 않으면서 PasswordEncoder를 사용하는 방법을 알아보자.  1. Dependency 추가 org.springframework.security spring-security-crypto Spring Security 전체를 디펜던시 추가하게 되면 인증 기능 자체를 스프링 시큐리티에게 위임하게 된다.그래서 얍삽이처럼 암호화 기능만 존재하는 spring-security-crypto 만 Dependency 추가를한다.  2. PasswordEncoder 빈 등록@Configu..
2024.07.10
[Java] 자바의 접근 제어자 종류 간단 정리
접근 제어자는 클래스, 변수, 메소드 등에 모두 적용할 수 있다. 1. public모든 클래스에서 접근 가능한 제어자 (같은 패키지든 다른 패키지든 상관없이 접근이 가능하다 Anywhere) 2. protected같은 패키지 내의 클래스에서 접근 가능단, 해당 접근 제어자가 지정된 클래스의 상속을 받은 즉, 서브 클래스에서도 접근이 가능하다.서브 클래스에서 접근이 가능할 땐 어떤 패키지든 상관없음 3. default (아무 접근 제어자도 명시하지 않았을 경우)같은 패키지 내에서만 접근 가능 4. private해당 클래스 내에서만 접근 가능
2024.07.10
no image
[Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (2/2)
이전 포스팅에서는 게시글 상세페이지에서 파일을 다운로드 하기 위해 게시글에 해당하는 파일 정보를 불러오는 기능을 추가하였다. 오늘은 드디어 핵심기능인게시글 상세페이지 내 파일을 클릭하면 다운로드를 할 수 있는 예제를 진행해보자. 이전 포스팅 참고https://just-joat.tistory.com/36  [Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (1/2)이전 포스팅인 게시판 파일 업로드 예제에 이어서이번에는 파일을 다운로드 하는 예제를 진행해보겠다. 따라서 DB 내에 게시글 테이블 및 파일 정보 테이블이 존재하고,파일 업로드 예제의 소just-joat.tistory.com 전체적인 흐름은 다음과 같다. 상세 페이지에서 파일을 클릭하면 해당 파일의 seq 값을 서버에..
2024.07.07
no image
[Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (1/2)
이전 포스팅인 게시판 파일 업로드 예제에 이어서이번에는 파일을 다운로드 하는 예제를 진행해보겠다. 따라서 DB 내에 게시글 테이블 및 파일 정보 테이블이 존재하고,파일 업로드 예제의 소스코드가 작성되어 있다고 가정한다. 이전 포스팅 참고https://just-joat.tistory.com/34 [Spring Boot] 스프링부트 & MyBatis 게시판 파일 업로드 예제기존에 게시판의 기본적인 생성, 조회 등의 기능은 구현되어 있다는 가정하에 진행하겠다. ※ 개발환경프레임워크: Spring Boot 2.7.12DB: MyBatis, MyBatisJava: JDK 1.8템플릿 엔진: Thymeleaf  1. DB 설계기존just-joat.tistory.com  파일을 다운로드 하기 위해서는 먼저, 게시글 ..
2024.07.07
no image
[Spring Boot] 스프링부트 & MyBatis 게시판 파일 업로드 예제
기존에 게시판의 기본적인 생성, 조회 등의 기능은 구현되어 있다는 가정하에 진행하겠다. ※ 개발환경프레임워크: Spring Boot 2.7.12DB: MyBatis, MyBatisJava: JDK 1.8템플릿 엔진: Thymeleaf  1. DB 설계기존에 게시판기능이 이미 구현이 되어 있으니 게시판 테이블은 다음과 같이 존재한다.ColumnDescriptionseq게시판 테이블의 기본키, serial 타입으로 인해 Auto Increament 속성 적용title게시글의 제목writer게시글의 작성자reg_date게시글의 작성일자count게시글의 조회수content게시글의 내용해당 테이블이 존재한다는 가정하에 파일 테이블을 다음과 같이 생성한다. ColumnDescriptionseq파일 테이블의 기본키,..
2024.07.06
no image
[MyBatis] Mapper XML 내 ResultMap 사용법
RestAPI를 통해 MyBatis로 테이블 셀렉 작업을 하는데 결과 값이 모두 Null값이어서 당황했다. 대체 왜이런건지 컨트롤러, 서비스, 매퍼 xml 등 살펴보다가 Select 쿼리 결과를 담는 DTO의 변수명들과 해당 테이블들의 칼럼명이 일치하지 않기 때문이라는 것을 알게되었다. @Getter@ToStringpublic class FileResponse { private int seq; private int boardSeq; private String originalName; private String saveName; private long size; private String deleteYn; private Timestamp createdDate; private Timestamp deleted..
2024.06.29
no image
[Spring Boot] MyBatis SQL 쿼리 로그 설정
Spring Boot 내에서 연동한 MyBatis의 쿼리 실행 로그를 볼 수 있도록 설정해보자. 1. Dependency 추가 org.bgee.log4jdbc-log4j2 log4jdbc-log4j2-jdbc4.1 1.16Log4jdbc Dependency를 추가한다. 2. application.properties DB 설정 변경spring.datasource.driver-class-name=org.postgresql.Driverspring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgresspring.datasource.username=postgresspring.datasource.password=1234기존의 applicati..
2024.06.29
728x90

사용자 기능 중 회원가입, 비밀번호 찾기 등에서 휴대폰 본인인증 과정에서 생성되는 6자리 인증번호를 만들어보자.

 

1. Random 객체를 통해 난수 추출


Random random = new Random(); // 랜덤 객체
int randomNum = random.nextInt(9) // 0~9 까지의 난수 1개 생성
  • 해당 객체를 통해 0에서 9까지의 한자리 숫자를 추출할 수 있다.
  • 추출되는 숫자는 정수형이다.

 

 

 

2. String 문자열 연산을 통해 한가지 씩 추출되는 난수를 결합


String authNum = ""; // 빈 문자열 생성                
authNum += Integer.toString(randomNum); // 생성된 난수를 String 타입으로 변환 후 문자열 연산
  • 앞서 생성한 Int형 난수를 문자열로 변환하여 기존 문자열에 결합한다.
  • 이러한 작업을 여섯 번 반복하면 6자리의 인증번호가 생성된다.

 

 

 

3. 전체 코드


    public String generateAuthNumber() {
        Random random = new Random();
        int createdNum = 0;
        String authNum = "";
        for (int i=0; i<=5; i++) {
            createdNum = random.nextInt(9);
            authNum += Integer.toString(createdNum);
        }
        return authNum;
    }

 

 

 

 

4. 결과


728x90
728x90

Spring 프로젝트를 개발하고 있는데,

인증 기능에 스프링 시큐리티 전체를 사용하지 않는다.

 

그래서 인증 기능을 따로 구현하게 됐는데

하지만, 비밀번호는 암호화를 해야하기 때문에

 

스프링 시큐리티에게 인증 제어를 받지 않으면서 PasswordEncoder를 사용하는 방법을 알아보자.

 

 

1. Dependency 추가


<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>		    
</dependency>
  • Spring Security 전체를 디펜던시 추가하게 되면 인증 기능 자체를 스프링 시큐리티에게 위임하게 된다.
  • 그래서 얍삽이처럼 암호화 기능만 존재하는 spring-security-crypto 만 Dependency 추가를한다.

 

 

2. PasswordEncoder 빈 등록


@Configuration
public class SpringSecurityConfig {
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

}
  • PasswordEncoder를 빈으로 등록해준다.
  • 설정 클래스를 새로 만들던 기존에 있는걸 쓰던 상관없다.
  • 필자는 스프링 부트 환경이라 설정 클래스를 쓰는데 xml 설정파일로 등록해도 상관없다.

 

 

3. 자유롭게 사용


@Autowired
private final PasswordEncoder passwordEncoder;

String encodedPassword = passwordEncoder.encode(param.get("password").toString());
  • 자유롭게 쓴다.
728x90
728x90

접근 제어자는 클래스, 변수, 메소드 등에 모두 적용할 수 있다.

 

1. public

  • 모든 클래스에서 접근 가능한 제어자 (같은 패키지든 다른 패키지든 상관없이 접근이 가능하다 Anywhere)

 

2. protected

  • 같은 패키지 내의 클래스에서 접근 가능
  • 단, 해당 접근 제어자가 지정된 클래스의 상속을 받은 즉, 서브 클래스에서도 접근이 가능하다.
  • 서브 클래스에서 접근이 가능할 땐 어떤 패키지든 상관없음

 

3. default (아무 접근 제어자도 명시하지 않았을 경우)

  • 같은 패키지 내에서만 접근 가능

 

4. private

  • 해당 클래스 내에서만 접근 가능
728x90
728x90

이전 포스팅에서는 게시글 상세페이지에서 파일을 다운로드 하기 위해 

게시글에 해당하는 파일 정보를 불러오는 기능을 추가하였다.

 

오늘은 드디어 핵심기능인

게시글 상세페이지 내 파일을 클릭하면 다운로드를 할 수 있는 예제를 진행해보자.

 

이전 포스팅 참고

https://just-joat.tistory.com/36 

 

[Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (1/2)

이전 포스팅인 게시판 파일 업로드 예제에 이어서이번에는 파일을 다운로드 하는 예제를 진행해보겠다. 따라서 DB 내에 게시글 테이블 및 파일 정보 테이블이 존재하고,파일 업로드 예제의 소

just-joat.tistory.com

 

전체적인 흐름은 다음과 같다.

 

  1. 상세 페이지에서 파일을 클릭하면 해당 파일의 seq 값을 서버에 요청
  2. 요청된 서버의 seq 값을 가지고 파일 테이블에서 파일의 정보 조회
  3. 조회된 파일의 정보를 이용해 자바의 Resource 인터페이스 구현체를 생성하여 리턴

* Resource 인터페이스: 컴퓨터 내 물리적으로 저장된 파일을 읽을 수 있는 인터페이스. 해당 인터페스의 구현체를 클라이언트에게 반환하면 파일을 다운로드 받을 수 있음.

 

 

1. FileMapper 인터페이스 수정 - 게시글 내 클릭한 파일의 상세정보 조회 메서드 추가


FileResponse getFileBySeq(int seq); // 파일 상세정보 조회
  • DB 내 파일 테이블에서 파일의 상세정보를 조회 할 수 있도록 메서드를 추가한다.
  • 리턴 타입은 당연히 FileResponse 이다.

 

 

2. FileMapper XML 수정 - 파일 SEQ 값을 참조하여 파일의 정보를 조회하는 쿼리 추가


<select id="getFileBySeq" resultMap="fileResponse">
    SELECT
        SEQ,
        <include refid="fileColumns"></include>
    FROM
        BOARD_FILE
    WHERE
        DELETE_YN = 'N'
        AND SEQ = #{seq}
</select>
  • 요청된 파일의 seq 값을 이용해 해당 seq의 파일 정보를 조회하는 쿼리를 추가한다. 
  • FileResponse 로 변환하기 위해 resultMap 설정하는거 잊지말자.

 

 

3. FileService 클래스 수정 - 메서드 추가


public FileResponse getFileBySeq(int seq) {
    return fileMapper.getFileBySeq(seq);
}
  • 파일 조회 메서드를 서비스에도 추가해준다.

 

 

4. FileUtils 클래스 수정 - 클라이언트에게 제공할 Resource 구현체 생성


//파일 Resource 객체 생성
public Resource readFileAsResource(final FileResponse file) {
    String uploadedDate = file.getCreatedDate().toLocalDateTime().format(DateTimeFormatter.ofPattern("yyMMdd"));
    String filename = file.getSaveName();
    Path filePath = Paths.get(uploadPath, uploadedDate, filename);

    try {
        Resource resource = new UrlResource(filePath.toUri());
        if(resource.exists() == false || resource.isFile() == false) {
            throw new RuntimeException("file not found: "+filePath.toString());
        }
        return resource;
    } catch (MalformedURLException e) {
        throw new RuntimeException("file not found: "+filePath.toString());
    }
}
  • 앞서 말했던 이번 예제에서 가장 중요한 Resource 구현체를 생성해주는 메서드이다.
  • Path filePath = Paths.get(uploadPath, uploadedDate, filename): 앞선 과정 중 DB에서 조회하여 파라미터로 넘어온 파일의 정보를 이용해 해당 파일이 저장되어 있는 파일의 경로
  • Resource resource = new UrlResource(filePath.toUri()): 위에서 만든 파일의 경로를 Resource 인터페이스의 구현체인 UrlResource 객체 생성자에 집어넣어 결국 클라이언트에게 제공할 수 있는 Resource 객체를 만들게 된다.
  • 생성한 Resource가 존재하지 않거나 파일이 아닐 경우에 인지할 수 있는 간단한 예외처리를 한다.

 

 

5. FileRestController 클래스 수정 - 파일 다운로드 매핑 메소드 추가


// 파일 다운로드
@GetMapping("fileDownload")
public ResponseEntity<Resource> downloadFile(@RequestParam("seq") int seq) {
    FileResponse file = fileService.getFileBySeq(seq);
    Resource resource = fileUtils.readFileAsResource(file);

    try {
        String filename = URLEncoder.encode(file.getOriginalName(), "UTF-8"); // 다운로드 시 파일명
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM) // HTTP MIME 타입 설정
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=\""+filename+"\";") // 헤더에 파일명 설정
                .header(HttpHeaders.CONTENT_LENGTH, file.getSize() + "") // 헤더에 파일크기 설정
                .body(resource);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException("filename encoding failed: "+file.getOriginalName());
    }
}
  • 다운로드 요청을 받고 앞서 추가한 메서드들을 호출해서 파일의 정보를 조회하고, 조회된 파일 정보를 가지고 Resource 객체를 만들어서 클라이언트에게 반환한다.

return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_OCTET_STREAM) // HTTP MIME 타입 설정
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=\""+filename+"\";") // 이 응답은 파일 다운로드
        .header(HttpHeaders.CONTENT_LENGTH, file.getSize() + "") // 헤더에 파일크기 설정
        .body(resource);
  • 클라이언트에게 반환되는 이 응답은 파일 다운로드이다. 라는 것을 알려주기 위한 HTTP 헤더 설정 방법이다.
  • HttpHeaders.CONTENT_DISPOSITION: 이 값을 HTTP 헤더에 설정해주어야 클라이언트가 파일을 다운로드 받을 수 있다.
  • HTTP 바디에는 우리가 만든 Resource를 넣어준다.

 

 

※ 결과


춘식이.png 다운로드
다운로드가 잘 받아졌다.

728x90
728x90

이전 포스팅인 게시판 파일 업로드 예제에 이어서

이번에는 파일을 다운로드 하는 예제를 진행해보겠다.

 

따라서 DB 내에 게시글 테이블 및 파일 정보 테이블이 존재하고,

파일 업로드 예제의 소스코드가 작성되어 있다고 가정한다.

 

이전 포스팅 참고


https://just-joat.tistory.com/34

 

[Spring Boot] 스프링부트 & MyBatis 게시판 파일 업로드 예제

기존에 게시판의 기본적인 생성, 조회 등의 기능은 구현되어 있다는 가정하에 진행하겠다. ※ 개발환경프레임워크: Spring Boot 2.7.12DB: MyBatis, MyBatisJava: JDK 1.8템플릿 엔진: Thymeleaf  1. DB 설계기존

just-joat.tistory.com

 

 

파일을 다운로드 하기 위해서는 먼저, 게시글 상세 페이지에서 

해당 게시글의 파일이 보여아 한다.

 

 

1. FileResponse 클래스 생성


@Getter
@ToString
public class FileResponse {
	
	private int seq;
	private int boardSeq;
	private String originalName;
	private String saveName;
	private long size;
	private String deleteYn;
	private Timestamp createdDate;
	private Timestamp deletedDate;
}
  • DB에 저장된 파일 테이블을 통해 파일정보를 클라이언트에게 응답해주기 위한 응답용 객체이다.

 

 

2. FileMapper 인터페이스 수정 - 게시글에 등록된 파일 조회 메서드 추가


@Mapper
public interface FileMapper {
	void saveAll(List<FileRequest> files); // 파일 정보 저장
	
	List<FileResponse> getFilesByBoardSeq(int boardSeq); // 게시글 파일 리스트 조회
}
  • List<FileResponse> getFilesByBoardSeq(int boardSeq): 게시글 기본키인 boardSeq 값을 참조하여 파일 테이블 내 해당 게시글의 파일들의 정보를 조회하는 메서드
  • 리턴 타입은 방금 생성한 FileResponse 클래스로 지정한다.

 

 

3. FileMapper XML 수정 - 게시글의 파일 조회 쿼리 추가


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.file.service.FileMapper">

	<sql id="fileColumns">
		BOARD_SEQ
		, ORIGINAL_NAME
		, SAVE_NAME
		, SIZE
		, DELETE_YN
		, CREATED_DATE
		, DELETED_DATE
	</sql>
	
	<insert id="saveAll" parameterType="List">
		INSERT INTO BOARD_FILE (
			<include refid="fileColumns"></include>
		) VALUES
		<foreach item="file" collection="files" separator=",">
		(
			#{file.boardSeq}
			, #{file.originalName}
			, #{file.saveName}
			, #{file.size}
			, 'N'
			, NOW()
			, NULL		
		)
		</foreach>
	</insert>
	
    <!-- getFilesByBoardSeq 조회 결과 테이블을 FileResponse 객체와 매핑 -->
    <resultMap type="egovframework.example.file.dto.FileResponse" id="fileResponse">  
		<result property="seq" column="SEQ"/>
		<result property="boardSeq" column="BOARD_SEQ"/>
		<result property="originalName" column="ORIGINAL_NAME"/>
		<result property="saveName" column="SAVE_NAME"/>
		<result property="size" column="SIZE"/>
		<result property="deleteYn" column="DELETE_YN"/>
		<result property="createdDate" column="CREATED_DATE"/>
		<result property="deletedDate" column="DELETED_DATE"/>
	</resultMap>
    
    <select id="getFilesByBoardSeq" resultMap="fileResponse"> <!-- 게시글 파일 조회 기능 추가 -->
		SELECT 
			SEQ,
			<include refid="fileColumns"></include>
		FROM
			BOARD_FILE
		WHERE
			DELETE_YN = 'N'
			AND BOARD_SEQ = #{boardSeq}
	</select>

</mapper>
  • 앞서 생성한 Mapper 인터페이스의 getFilesBoardSeq() 메서드와 연결할 쿼리를 작성한다.
  • 게시글 테이블의 기본키인 BOARD_SEQ 칼럼을 참조하여 해당 게시글의 해당되는 파일들을 조회한다. 
  • 조회 결과를 FileResponse 객체로 변환하여야 하기 때문에 ResultMap 태그를 작성해준다.

 

 

4. FileService 클래스 수정 - 게시글에 등록된 파일 조회 메서드 추가


public List<FileResponse> getFilesByBoardSeq(int boardSeq) { // 파일 정보 조회
    return fileMapper.getFilesByBoardSeq(boardSeq);
}
  • Mapper 인터페이스를 통해 XML 쿼리까지 연결되기 위해 FileService 클래스에도 메서드를 추가해준다.

 

 

5. FileRestController 클래스 생성


@RestController
@RequiredArgsConstructor
public class FileRestController {
	
	private final FileService fileService;
	
	@GetMapping("/fileList")
	public ResponseEntity<Object> getFileList(@RequestParam("boardSeq") int seq) {
		List<FileResponse> files = new ArrayList<>();

		files = fileService.getFilesByBoardSeq(seq);
		
		return ResponseEntity.status(HttpStatus.OK).body(files);

	}
}
  • 파일 정보 조회 요청을 받을 컨트롤러를 생성한다.
  • 해당 컨트롤러는 API 형식으로 통신이 진행될 것이기 때문에 @RestController 어노테이션을 지정해준다.
  • ResponseEntity 객체를 통해 게시글의 파일들의 정보를 JSON 형태로 리턴한다.

 

 

6. 게시글 상세 페이지 HTML 수정 - 게시글 파일 정보 영역 및 AJAX 통신 함수 추가


            <tr>
                <td>첨부파일</td>
                <td id="files"></td>
            </tr>
  • 다음과 같이 게시글 상세 페이지에 파일 정보가 보이게 될 영역을 추가한다

    window.onload = () => { // 상세 페이지가 로드되면 파일정보 조회 API 실행
        getFiles();
    }

    function getFiles() { // AJAX 통신을 통해 파일정보를 불러옴.
        let seq = [[${board.seq}]] || '';
        $.ajax({
            type: 'get',
            url: `/fileList?boardSeq=${seq}`,
            success: function(result) {
                console.log(result);
                setFileInfo(result)
            }
        })
    }

    function setFileInfo(files) { // HTML 내 파일영역에 불러온 파일정보 삽입
        let fileHtml = '<div class="file_down"><div class="cont">';
        files.forEach(row => {
            fileHtml += `<a>${row.originalName}</a>`
        })

        fileHtml += '</div></div>'

        document.getElementById('files').innerHTML = fileHtml
    }
  • 게시글 상세 페이지가 로드되면 getFiles() -> setFileInfo() 함수가 순서대로 실행된다.
  • getFiles(): AJAX를 통해 FileController에 파일 정보를 요청한다.
  • setFileInfo(): AJAX 요청이 성공하면 응답받은 파일 정보를 HTML 내 파일 영역에 삽입된다. 

 

 

※ 결과


춘식이.png가 잘 보인다.

 

 

파일 다운로드 기능은 다음 포스팅에서 다루겠다.

728x90
728x90

기존에 게시판의 기본적인 생성, 조회 등의 기능은 구현되어 있다는 가정하에 진행하겠다.

 

※ 개발환경

  • 프레임워크: Spring Boot 2.7.12
  • DB: MyBatis, MyBatis
  • Java: JDK 1.8
  • 템플릿 엔진: Thymeleaf

 

 

1. DB 설계


  • 기존에 게시판기능이 이미 구현이 되어 있으니 게시판 테이블은 다음과 같이 존재한다.

Column Description
seq 게시판 테이블의 기본키, serial 타입으로 인해 Auto Increament 속성 적용
title 게시글의 제목
writer 게시글의 작성자
reg_date 게시글의 작성일자
count 게시글의 조회수
content 게시글의 내용

  • 해당 테이블이 존재한다는 가정하에 파일 테이블을 다음과 같이 생성한다.

 

Column Description
seq 파일 테이블의 기본키, serial 타입으로 인해 Auto Increament 속성 적용
board_seq 게시판 테이블의 기본키를 참조하는 외래키, 게시글마다 첨부파일을 연결하기 위한 칼럼
original_name 업로드하는 첨부파일의 원본 파일명
save_name 컴퓨터 내 물리적으로 보관될 파일명
size 파일의 크기
delete_yn 파일의 삭제 여부
created_date 파일의 생성일자
deleted_date 파일의 삭제일자
  • 여기서 핵심적인 칼럼은 original_name save_name이다. 두 칼럼 모두 파일의 이름을 의미하지만, DB 테이블에만 저장되는 original_name과 컴퓨터 내 물리적으로 저장되는 save_name은 서로 달라야 한다.
  • 예를 들어, 서로 다른 사용자가 "춘식이.jpg" 라는 이름의 파일을 저장했을 때 이 첨부파일의 이름을 그대로 서버 컴퓨터 내 저장하게 되면 각각 어느 사용자가 업로드한 춘식이인지 구별할 수 없다. 그래서 실제로 저장되는 파일명은 랜덤값을 통해 고유한 식별값으로 지정되어야 한다.

 

 

2. FileRequest 클래스 생성


@Getter
public class FileRequest {

	private int boardSeq; 			// 게시글 번호(FK) 
	private String originalName; 	// 원본 파일명
	private String saveName;		// 저장 파일명
	private long size;				// 파일 크기
	
	
	
	@Builder
	public FileRequest(String originalName, String saveName, long size) {
		this.originalName = originalName;
		this.saveName = saveName;
		this.size = size;
	}



	public void setBoardSeq(int boardSeq) {
		this.boardSeq = boardSeq;
	}

}
  • 앞서 생성한 파일 테이블에 저장하기 위한 파일 정보를 담을 용도인 FileRequest 클래스이다.
  • 파일 정보를 손쉽게 설정하기 위해 빌더패턴을 사용한다.
  • boardSeq(게시글 번호)는 게시글이 생성된 후에 설정을 해줘야 하기 때문에 파일 정보들과 설정 시기가 맞지 않아 따로 Setter 메소드로 제외한다. 

 

 

3. FileMapper 인터페이스 생성


@Mapper
public interface FileMapper {

	void saveAll(List<FileRequest> files); // 파일 정보 저장

}
  • saveAll(List<FileRequest files): 들어온 FileRequest 클래스를 통해 파일의 정보를 DB에 저장한다. 게시판에 다중 파일 첨부 기능을 도입할 것이기 때문에 여러 파일 정보를 저장하기 위해 List<FileRequest> 타입으로 받는다.

 

 

4. FileMapper XML 생성


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.file.service.FileMapper">

	<sql id="fileColumns">
		BOARD_SEQ
		, ORIGINAL_NAME
		, SAVE_NAME
		, SIZE
		, DELETE_YN
		, CREATED_DATE
		, DELETED_DATE
	</sql>
	
	<insert id="saveAll" parameterType="List">
		INSERT INTO BOARD_FILE (
			<include refid="fileColumns"></include>
		) VALUES
		<foreach item="file" collection="files" separator=",">
		(
			#{file.boardSeq}
			, #{file.originalName}
			, #{file.saveName}
			, #{file.size}
			, 'N'
			, NOW()
			, NULL		
		)
		</foreach>
	</insert>
	

</mapper>
  • 여러개의 파일을 저장할 것이기 때문에 List 형태로 넘어온 FileRequest를 <foreach>태그로 한꺼번에 INSERT 한다.

 

 

5. FileService 클래스 생성


@Service
@RequiredArgsConstructor
public class FileService {
	private final FileMapper fileMapper;
	
	
	@Transactional
	public void saveFiles(final int boardSeq, final List<FileRequest> files) {
		if(CollectionUtils.isEmpty(files)) {
			return;
		}
		for(FileRequest file : files) {
			file.setBoardSeq(boardSeq);
		}
		fileMapper.saveAll(files);
	}
	
}
  • 파일을 업로드할 게시글의 번호와 파일 정보를 받아 fileMapper의 saveAll 메소드를 호출하여 DB까지 연결되도록 한다.
  • saveAll(files)를 통해 여러개의 파일이 삽입되던 중 한가지의 파일을 삽입하다가 문제가 발생되면 해당 메소드 호출 이전으로 롤백될 수 있도록 @Transactional 어노테이션을 사용한다.

 

 

6. FileUtils 클래스 생성


@Component
public class FileUtils {
	
	// 물리적으로 파일을 저장할 위치(C:\\boardPrc\\upload-files)
	private final String uploadPath = Paths.get("D:", "boardPrc", "upload-files").toString();
	
	
	// 다중 파일 업로드
	public List<FileRequest> uploadFiles(final List<MultipartFile> multipartFiles) {
		List<FileRequest> files = new ArrayList<>();
		for (MultipartFile multipartFile : multipartFiles) {
			if(multipartFile.isEmpty()) {
				continue;
			}
			files.add(uploadFile(multipartFile));
		}
		
		
		return files;
		
	}
	
	// 단일 파일 업로드
	public FileRequest uploadFile(final MultipartFile multipartFile) {
		if(multipartFile.isEmpty() ) {
			return null;
		}
		
		// 디스크(폴더)에 저장할 파일명
		String saveName = generateSaveFilename(multipartFile.getOriginalFilename());
		// 오늘날짜 (240706)
		String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")).toString();
		// 파일의 업로드 경로 (D:\\boardPrc\\upload-files\\240706\\saveName)
		String uploadPath = getUploadPath(today) + File.separator + saveName;
		// 업로드할 경로의 파일 객체
		File uploadFile = new File(uploadPath);
		
		try {
			multipartFile.transferTo(uploadFile); // 파일 업로드
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		return FileRequest.builder() // 업로드 한 파일의 정보를 빌더패턴으로 DTO에 담는다.
				.originalName(multipartFile.getOriginalFilename())
				.saveName(saveName)
				.size(multipartFile.getSize())
				.build();
	}
	
	// saveName 생성 (실제로 폴더에 저장될 파일명)
	private String generateSaveFilename(final String filename) {
		String uuid = UUID.randomUUID().toString().replaceAll("-", ""); // 파일의 식별을 위해 랜덤값 지정
		String extension = StringUtils.getFilenameExtension(filename);
		return uuid + "." + extension;
	}
	
	private String getUploadPath() {
		return makeDirectories(uploadPath);
	}
	
	private String getUploadPath(final String addPath) {
		return makeDirectories(uploadPath + File.separator + addPath);
	}
	
	private String makeDirectories(final String path) {
		File dir = new File(path);
		
		if(dir.exists() == false) {
			dir.mkdirs();
		}
		return dir.getPath();
	}
  • 파일을 직접 MultipartFile 객체를 이용해 서버 컴퓨터 내 물리적으로 업로드를 하고, 업로드 할 파일의 정보를 가지고 FileRequest 객체를 만드는 클래스이다.
  • 이곳에서 만들어진 FileRequest 객체를 가지고 DB의 파일 정보 테이블에 삽입하게 된다.
  • String uploadPath = Paths.get("D:", "boardPrc", "upload-files").toString(): 물리적으로 파일을 저장할 경로를 담는 변수이다. 
  • FileRequest uploadFile(MultipartFile multipartFile): 물리적으로 저장되는 파일명인 saveName 및 추가적인 폴더명을 생성하여 만든 파일 저장 경로를 가지고 서버 컴퓨터 내 실제로 파일을 업로드하고, 업로드 된 정보를 빌더 패턴을 통해 FileRequest 객체로 변환하여 리턴한다.
  • List<FileRequest> uploadFiles(List<MultipartFile> multipartFiles): MultipartFile 객체가 여러개일 경우 List 형태로 받아서 반복문으로 각 파일을 uploadFile() 메소드를 사용하여 다중 파일 처리를 하는 메소드이다.
  • String generateSaveFilename(String filename): 서버 컴퓨터 내 폴더에 물리적으로 저장할 파일명을 생성하는 메소드이다. 앞서 설명했듯이 디스크에 저장되는 파일명들은 각 고유한 식별값을 가져야 하므로 UUID라는 랜덤값을 이용해 생성한다.
  • String getUploadPath(): 맨 끝 상단에서 정의한 uploadPath 변수에 설정된 경로를 리턴하는데, 추가적인 경로 문자열을 파라미터로 넘기면 해당 경로를 추가하여 채로 리턴한다.
  • String makeDirectories(String path): getUploadPath() 메소드에서 사용하는 메소드로, 경로를 파라미터로 받고 해당 경로가 존재하지 않으면 폴더를 새로 생성하고, 존재하면 경로 그대로를 다시 리턴한다.

 

 

7. BoardController 게시글 등록 매핑 메소드 수정


@PostMapping("/registBoard")
public String registBoard(
        @RequestParam("title") String title,
        @RequestParam("writer") String writer,
        @RequestParam("content") String content,
        @RequestParam("files") List<MultipartFile> multipartFiles) {
    Map<String, Object> board = new HashMap<>();
    board.put("title", title);
    board.put("writer", writer);
    board.put("content", content);
    try {
        boardService.registBoard(board, multipartFiles);


    } catch (Exception e) {
        e.printStackTrace();
    }
    return "redirect:/";
}
  • 파일을 처리하는 역할의 모든 코드들을 완성했다. 이제 게시글 등록 Controller와 Service를 수정해주면 된다.
  • Controller는 클라이언트를 통해 넘어온 파일을 MultipartFile 객체 형태로 받을 수 있게 수정해주면 된다.
  • @RequestParam("files") List<MultipartFile> multipartFiles: 클라이언트를 통해 넘어온 "files" 라는 이름의 데이터를 MultipartFile 형태로 받는다.
  • boardService.registBoard(board, multipartFiles): 게시글을 등록하는 Service 메소드 내에서 파일 처리 메소드를 호출할 것이기 때문에 multipartFiles 변수를 파라미터로 추가한다.

 

 

8. BoardService 게시글 등록 서비스 메소드 수정


public void registBoard(Map<String, Object> board, List<MultipartFile> multipartFiles) {

    boardMapper.registBoard(board);
    List<FileRequest> files = fileUtils.uploadFiles(multipartFiles);
    fileService.saveFiles((int) board.get("SEQ"), files);

}
  • BoardService의 registBoard() 메소드는 기존에 boardMapper를 통해 게시글만 DB에 삽입하는 역할 뿐만 아니라 MultipartFile 객체를 처리하여 파일 업로드, 파일정보 DB 삽입 기능 등을 FileUtils와 FileService 클래스에게 직접 위임하는 역할이 추가되었다.
  • List<FileRequest> files = fileUtils.uploadFiles(multipartFiles): MultipartFile 객체를 앞서 작성한 FileUtils의 uploadFiles() 메소드에게 넘기며 호출한다.
  • fileService.saveFiles((int) board.get("SEQ"), files): 생성된 게시글의 기본키 값과 생성된 FileRequest를 가지고 FileService에게 DB에 삽입되도록 saveFiles() 메소드를 호출한다.

 

 

9. application.properties 수정 - 파일 사이즈 설정


spring.servlet.multipart.maxFileSize=10MB
spring.servlet.multipart.maxRequestSize=50MB
  • 스프링 부트에서는 요청되는 MultipartFile 객체 파일의 사이즈에 제한이 되어있는데, 요청에 포함되는 파일 한 개당 최대 1MB, 전체 파일의 합은 최대 10MB로 설정되어 있다.
  • 이를 한 개당 10MB, 총 50MB로 바꿔보자.

 

 

10. 게시글 등록 폼에 파일 영역 추가


    <form action="/registBoard" method="post" enctype="multipart/form-data">
        <label for="title">제목:</label>
        <input type="text" id="title" name="title" required>
        <br><br>
        
        <label for="author">작성자:</label>
        <input type="text" id="author" name="author" required>
        <br><br>
        
        <label for="content">내용:</label>
        <textarea id="content" name="content" rows="10" cols="50" required></textarea>
        <br><br>
        
        <label for="files">파일 선택:</label>    <!-- 파일 영역 추가 -->
        <input type="file" id="files" name="files" multiple>
        <br><br>
        
        <input type="submit" value="작성">
    </form>

 

 

 

※ 결과


게시판 테이블
파일 테이블, board_seq 칼럼 값이 게시글 테이블의 seq와 외래키로써 같은 것을 볼 수 있다.
앞서 설장한 파일 경로에 파일 테이블의 save_name 값으로 파일명이 생성되고, 춘식이 사진이 잘 업로드 되었다.

728x90
728x90

RestAPI를 통해 MyBatis로 테이블 셀렉 작업을 하는데 결과 값이 모두 Null값이어서 당황했다.

Null이 풍년

 

대체 왜이런건지 컨트롤러, 서비스, 매퍼 xml 등 살펴보다가 

Select 쿼리 결과를 담는 DTO의 변수명들과 해당 테이블들의 칼럼명이 일치하지 않기 때문이라는 것을 알게되었다.

 

@Getter
@ToString
public class FileResponse {
	private int seq;
	private int boardSeq;
	private String originalName;
	private String saveName;
	private long size;
	private String deleteYn;
	private Timestamp createdDate;
	private Timestamp deletedDate;
}

DTO의 변수명과 테이블 칼럼명이 다르다.

 

이렇게 서로 다를 경우에는 내가 만든 DTO에 테이블 조회 결과값들을 못 담는다.

 

그래서 Mapper XML 내에 따로 작업을 해줘야 하는데 그 방법이 바로 ResultMap 태그를 이용하는 것이다.

 

 

1. Mapper.xml 내 ResultMap 설정

	<resultMap type="com.example.file.dto.FileResponse" id="fileResponse">
		<result property="seq" column="SEQ"/>
		<result property="boardSeq" column="BOARD_SEQ"/>
		<result property="originalName" column="ORIGINAL_NAME"/>
		<result property="saveName" column="SAVE_NAME"/>
		<result property="size" column="SIZE"/>
		<result property="deleteYn" column="DELETE_YN"/>
		<result property="createdDate" column="CREATED_DATE"/>
		<result property="deletedDate" column="DELETED_DATE"/>
	</resultMap>
  • type="com.example.file.dto.FileResponse":  쿼리 조회 결과를 담을 DTO 클래스 경로를 입력한다.
  • id="fileResponse": 해당 ResultMap 태그의 독자적인 식별 아이디를 입력한다.
  • property="DTO 변수명": 칼럼과 연결할 DTO의 변수명을 입력한다.
  • column="칼럼명": DTO 변수와 연결할 칼럼명을 입력한다.

 

2. Mapper.xml 내 Select 태그 수정

	<select id="getFilesByBoardSeq" resultMap="fileResponse">
		SELECT 
			SEQ,
			<include refid="fileColumns"></include>
		FROM
			BOARD_FILE
		WHERE
			DELETE_YN = 'N'
			AND BOARD_SEQ = #{boardSeq}
	</select>
  • resultMap="fileResponse": 앞서 정의한 resultMap 태그의 id 값을 입력한다.

 

※ 결과

728x90
728x90

Spring Boot 내에서 연동한 MyBatis의 쿼리 실행 로그를 볼 수 있도록 설정해보자.

 

1. Dependency 추가

<dependency>
    <groupId>org.bgee.log4jdbc-log4j2</groupId>
    <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
    <version>1.16</version>
</dependency>
  • Log4jdbc Dependency를 추가한다.

 

2. application.properties DB 설정 변경

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=1234
  • 기존의 application.properties 내에서 DB 설정 부분이 이렇게 되어 있었다면 수정할 부분은 딱 2가지가 된다.
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.jdbc-url=jdbc:log4jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=1234
  • spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy - 드라이버 클래스 이름 변경
  • spring.datasource.jdbc-url=jdbc:log4jdbc:postgresql://localhost:5432/postgres - url 주소에 'log4jdbc' 삽입

 

3. log4jdbc.log4j2.properties 생성

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
  • resources 폴더 바로 밑에 생성해준다.
  • 다음 설정을 입력해둔다.

 

4. logback.xml 생성 및 설정

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="STDOUT"
		class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{yyyyMMdd HH:mm:ss.SSS} [%thread] %-3level %logger{5} - %msg %n</pattern>
		</encoder>
	</appender>
	<logger name="jdbc" level="OFF" />
	<logger name="jdbc.sqlonly" level="OFF" />
	<logger name="jdbc.sqltiming" level="DEBUG" />
	<logger name="jdbc.audit" level="OFF" />
	<logger name="jdbc.resultset" level="OFF" />
	<logger name="jdbc.resultsettable" level="DEBUG" />
	<logger name="jdbc.connection" level="OFF" />
	<root level="INFO">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>
  • 쿼리 로그를 어떻게 보여줄지에 대해 직접적으로 설정하는 파일이다.
  • 이것도 마찬가지로 resources 폴더 바로 밑에 생성한다.
  • 가장 중요한 3가지 설정을 보자면,
  • jdbc.sqlonly: 실행된 SQL 문만을 로그로 출력할지 여부
  • jdbc.sqltiming: 실행된 SQL문과 함께 해당 쿼리를 실행하는 데 걸린 시간을 로그로 출력할지 여부 
  • jdbc.resultsettable: SELECT 쿼리 결과인 ResultSet의 내용을 테이블 형식으로 로그에 출력할지 여부

 

※ 결과

728x90