Spock Framework

 

개요 및 특징
  • Groovy 언어를 기반으로 자바와 100% 호환됨
  • mocking, stubbing 을 지원해 mock객체로 동작 정의가 쉬움
  • 라이프사이클 메소드 제공 : setup(), cleanup() 등의 메소드로 테스트 전/후 설정 수행이 가능
  • 동일한 데이터를 여러번 실행가능한 블록 제공
  • 테스트 블록을 분리하고 실행 순서 지정 가능

 

 

Spock Block
  • given / setup
    • 테스트에 필요한 객체나 환경을 준비하는 블록
  • when
    • 테스트 하고 싶은 상황을 만드는 영역 ( 코드 실행 영역 )
  • then
    • 테스트 결과를 검증하는 블록 ( assert에 해당하는 문장 )
  • expect
    • when, then이 합쳐진 형태로 단위 테스트 시 유용
  • cleanup
    • 필요시 setup 자원을 정리
  • where
    • 일부 데이터만 바꿔가며 테스트를 할 수 있게 도와주는 영역

 

 

기본 사용법

 

1. 라이브러리 추가

testImplementation "org.spockframework:spock-spring"

 

2. 플러그인 추가

apply plugin: 'groovy' // groovy 지원

 

3. Specification 클래스 상속

import spock.lang.Specification

class UpdateBizFleetOrderVinsToPurchasedHandlerTest extends Specification {
}

* 이때, 주의할 점은 클래스를 ctrl + command + T 로 만들 때, 기존에 Test Library JUnit으로 사용하시던 분들은 Groovy로 바꿔줘야 합니다.

 

 

4. 라이프 사이클에 맞는 메소드 생성

def setupSpec() {}    // 모든 테스트 케이스 실행 전 실행 (@BeforeClass)
def setup() {}        // 각 테스트 케이스 실행 전 마다 실행 (@Before)
def cleanup() {}      // 각 테스트 케이스 실행 후 마다 실행 (@After)
def cleanupSpec() {}  // 모든 테스트 케이스 실행 후 실행 (@AfterClass)

 

 

참고
  • @Shared
    • 여러 테스트 간에 걸쳐서 공유되는 객체
  • Mock 테스트 코드
    • 도메인 클래스를 Mock 클래스의 피라미터로 주면서 생성
    • >> 를 통해 값을 반환 ( ex. A.findName(1) >> "park" )
SqlSessionFactory

SqlSessionFactory 에서 만들어지는 SqlSession 객체가 담당한다.

그리고 SqlSession 객체를 생성하는 시점에 트랜잭션에 관련한 속성을 결정한다.

 

 

SqlSessionFactory class 가 제공하는 openSession()

1. openSession(): 마이바티스 설정 파일의 설정을 그대로 사용하는 바이바티스 객체 생성

2. openSession(boolean autoCommit): 마이바티스 설정을 그대로 사용하되 자동 커밋 여부를 변경함

3. openSession(Connection connection)
: 마이바티스 연결 정보를 갖는 커넥션 타입 객체를 피라미터로 전달해서 마이바티스 객체 생성
: 만들어진 객체만큼 연결 객체가 갖는 여러 정보를 생성 시점에 설정 가능
: 연결 정보를 직접 사용하기 때문에 "마이바티스와 별도로 트랜잭션 제어 가능"

4. openSession(TransactionIsolationLevel level)
: 트랜잭션 격리 레벨을 지정해서 마이바티스 객체를 생성

그 외에 여러 메소드가 있음

 

 

openSession 메소드를 사용해 객체를 생성하게 되면 다음과 같은 특징을 가지게 됩니다.

 

- 객체를 생성할 때마다 트랜잭션 시작

- 마이바티스 설정 파일로 데이터 소스 사용

- 트랜잭션 관련 격리 레벨이나 전파 설정은 설정한 값을 사용해 설정됨

 

 

 

트랜잭션을 처리하는 메소드

1. commit(): 데이터를 입력, 수정, 삭제한 내용을 데이터베이스에 물리적으로 반영 (마이바티스가 변경한적 있다고 판단해야 처리함)

2. commit(boolean force): 내부적으로 데이터를 변경한 적 있는지 판단하지 않고 무조건 처리함

3. rollback(): 트랜잭션 롤백해서 직전까지 데이터를 반영하지 않고 무시함 마이바티스가 변경한적 있다고 판단해야 처리함)

4. rollback(boolean force): 내부적으로 데이터를 변경한 적 있는지 판단하지 않고 무조건 처리함

 

 

 

 

1. @Tag

@Tag(name="tag_name", description="tag_description")

Tag 에 설정된 name이 같은 것끼리 하나의 api 그룹으로 묶음.

주로 Controller 영역에 설정함

 

 

 

2. @Schema

@Schema(description="한글이름", defaultValue="기본값", allowableValues="")

모델에 대한 정보를 작성하는 곳이다.

 

 

 

3. @Operation

@Operation(summary="api에 대한 간략한 설명", description="api에 대한 상세 설명", response="api response list", parameter="api parameter list")

api 동작에 대한 명세를 작성하기 위해 Controller 메소드에 설정하는 곳이다.

 

  • summary : swagger UI가 접혀있을 때, 간단히 확인할 수 있는 정보
  • description : 필요에 따라 상세 정보를 표기하고자 할 때 추가

 

 

 

4. @ApiResponse

@ApiResponse(responseCode="HTTP status code", description="response에 대한 설명", content="response payload 구조")

단순히, 200 코드만 제공할게 아니라면 아래와 같이 적어주면 된다.

 

  • 예제
ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "유저 조회 성공", content = @Content(schema = @Schema(implementation = users.class))),
        @ApiResponse(responseCode = "404", description = "존재하지 않는 유저 조회", content = @Content(schema = @Schema(implementation = users.class))) })
@Operation(summary = "유저 조회", description = "id를 이용하여 유저를 조회합니다.")
@GetMapping("/users/{id}")
public Users findById(@PathVariable Long id) {
    return userService.findById(id);
}

 

 

 

5. @Parameter

@Parameter(name="파라미터 이름", description="파라미터 설명", in="파라미터 위치")
  • @ApiResponse 처럼 parameter 리스트를 담아 API 설정
  • @Operation 처럼 parameter 요소에 설정 가능

쉽게 말해, api 파라미터 리스트를 보여준다.

 

예제:

    @Operation(description = "로그인")
    @PostMapping(value = "/signin")
    public ResponseEntity<UserResponse> signIn(@Parameter @RequestBody UserRequest userRequest) {
        return ResponseEntity.ok(userService.signIn(userRequest));
    }

만약, 해당 파라미터를 API 문서에 보여주고 싶지 않은 경우, @Parameter(hidden=true) 설정을 해주면 됨

 

 

 

 

 

'스터디' 카테고리의 다른 글

서블릿과 스프링MVC  (0) 2023.01.01
CSRF  (0) 2022.12.31
세션과 쿠키, JWT  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01

WHERE 절이 OUTER JOIN에서 데이터를 누락시키는 주요 이유 중 하나는 OUTER JOIN의 작동 방식과 관련이 있습니다.

 

OUTER JOIN은 두 개 이상의 테이블을 연결할 때 사용되며,

특히 어느 한 쪽 테이블에 매칭되는 데이터가 없더라도 결과를 반환합니다.

이러한 특성 때문에 WHERE 절에서 조건을 지정하면 OUTER JOIN의 결과를 필터링할 수 있습니다.

 

그러면 왜 OUTER JOIN에서 데이터를 누락시키는지 살펴보겠습니다

 

ex.

SELECT d.deptno, sum(e.sal)
FROM dept d LEFT OUTER JOIN emp e
ON d.deptno = e.deptno
WHERE e.sal > 2000
GROUP BY d.deptno
ORDER BY d.deptno;

: 이런 경우 deptno 가 10, 20, 30에 값이 있고, deptno 40에 e.sal 값이 null일 때 deptno 40컬럼에 대한 데이터가 누락됩니다.

 

  1. NULL 값 필터링: OUTER JOIN에서 한 쪽 테이블과 매칭되는 데이터가 없는 경우, 해당 행의 다른 쪽 테이블의 열은 NULL 값을 가집니다. WHERE 절을 사용하여 NULL이 아닌 행만 선택하면, 데이터의 일부가 누락될 수 있습니다.
  2. 결과 제한: WHERE 절을 사용하여 결과를 필터링하면 OUTER JOIN 결과에서 특정 행을 제외할 수 있습니다. 예를 들어, WHERE 절에서 조건을 만족하지 않는 행을 필터링하면 OUTER JOIN 결과에 해당 행이 나타나지 않게 됩니다.
  3. 조인 조건: OUTER JOIN의 결과를 필터링하기 위해 WHERE 절에 조인 조건을 추가할 수 있습니다. 조인 조건을 만족하지 않는 행은 결과에서 제외됩니다.
  4. 의도한 필터링: 때로는 OUTER JOIN 결과를 일부 필터링하여 특정 데이터만 가져오기를 원할 수 있습니다. 이 경우 WHERE 절을 사용하여 결과를 원하는 대로 제한할 수 있습니다.

OUTER JOIN과 WHERE 절을 함께 사용할 때 주의해야 합니다.

데이터 누락을 방지하려면 OUTER JOIN 조건과 WHERE 절을 조심스럽게 설계해야 합니다. 원하는 결과를 얻기 위해 조건을 올바르게 설정하는 것이 중요합니다.

'mybatis' 카테고리의 다른 글

트랜잭션 관리  (0) 2023.09.14
mybatis associate, collection, alias  (0) 2023.09.12
마이바티스란? (과정 및 개요)  (0) 2023.09.10
Spring + MySQL + Mybatis  (0) 2023.08.17
Association

has one 관계를 형성하기 위함

 

 

Collection

has many 관계를 형성하기 위함

 

 

Mybatis 에서 관게를 정의하는 방법
  • Nested Select : 1번의 추가 Select 를 통한 데이터 검색
  • Nested Results : Join 을 통한 한 번에 데이터 검색

2번의 경우, association 태그 내에서 resultMap에 지정한 형태로 결과 값을 담게 된다.

 

 

도메인 관계

[User] 1:1 [Board]
[Apply] N:1 [Board]

@Data
public class User {
 private Long id;
 pirvate String name;
}

@Data
public class Board {
 private Long id;
 private String name;
 private User user;  // has one relation ( association )
 private List<Apply> apply; // has many relation ( collection )
}

public class Apply {
 private Long id;
 private Board board; // has one relation ( association )
 private User user; // has one relation ( association )
}

 

 

<mapper namespace="package.BoardRepo">    

    <resultMap id="userResultMap" type="package.users">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
    </resultMap>
    
    
    
    <resultMap id="BoardResultMap" type="package.Board">
    	<id column="board_id" property="id" jdbcType="BIGINT"/>
        <result column="board_name" property="name" jdbcType="VARCHAR"/>
        
        // nested Result
        <collection column="applys" property="applys" ofType="hashmap" javaType="list">
        	<id column="id" property="id" jdbcType="BIGINT"/>
            
            // nested Result
            <assocation property="board" resultMap="BoardResultMap"/>
            
            //nested Select
            <assocation column="user_id" property="user" select="selectUserById"/>
        </collection>

        // nested Result
        <assocation property="user" resultMap="userResultMap"/>
    </resultMap>
    
    
    
    <resultMap id="applyResultMap" type="package.applys">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <assocation property="board" resultMap="BoardResultMap"/>
        <assocation column="user_id" property="user" select="selectUserById"/>
    </resultMap>



    <select id="selectUserById" resultMap="UserResultMap2" parameterType="java.lang.Long">
        SELECT
            id, name
        FROM
            users
        WHERE
            id = #{id, jdbcType=BIGINT}
    </select>
</mapper>

 

  • <association property="user" resultMap="userResultMap"/>

이 의미는 Board 클래스에 User타입의 user 변수에 resultMap으로 선언한 userResultMap 형태의 값을 바인딩시킨다.

즉, user에 userResultMap의 값이 들어간다.

 

 

  • <assocation column="user_id" property="user" select="selectUserById"/>

이 의미는 해당 쿼리를 수행하기 전에 selectUserById 쿼리를 먼저 수행해준다.

 

 

 

${alias}

동적 SQL 쿼리를 작성할 때 사용하는 "플레이스 홀더" 이다.

이를 통해 SQL 일부를 "동적" 으로 생성하거나 피라미터로 값을 삽입 가능하다.

 

즉, '${alias}' 문법은 문자열 치환을 수행한다.

 

<select id="findUsersByAlias" parameterType="string" resultType="com.example.User">
  SELECT * FROM users WHERE alias = #{alias}
</select>

이 매퍼는 findUsersByAlias라는 메소드에서 alias라는 파라미터를 받아 테이블에서 alias에 해당하는 사용자를 검색한다.

 

이를 수행하는 코드:

 

String alias = "john_doe"; // 파라미터로 전달할 alias 값
User user = sqlSession.selectOne("findUsersByAlias", alias);

이렇게 실제로 alias라는 파라미터를 전달해야 합니다.

 

이는 동적 검색, 정렬 순서와 같이 일부 조건을 바꿔야 할 때 유용합니다.

 

 

'mybatis' 카테고리의 다른 글

트랜잭션 관리  (0) 2023.09.14
WHERE 절은 왜 OUTER JOIN 에서 데이터를 누락시키는가?  (0) 2023.09.13
마이바티스란? (과정 및 개요)  (0) 2023.09.10
Spring + MySQL + Mybatis  (0) 2023.08.17

개요

- 마이바티스는JDBC를 대체하는 Persistence Framework이다.

- 즉, 개발자는 SQL문을 작성하고 Mybatis는 JDBC를 사용해 실행한다. ( 아래에 과정을 참조 )

- ORM 프레임워크가 아닌 SQL 매퍼이다. ( ORM은 객체 간의 관계를 자동으로 설정해주지만 Mybatis는 명시 해줘야함 )

- 트랜잭션을 관리한다.

 

 

 

Mybatis를 왜 사용하는가?

- JDBC 코드를 작성하는 일에는 코드를 복사하고 붙쳐넣으며 많은 시간이 소요된다. 그리고 그만큼 오류도 빈번히 발생한다.

- 마이바티스는 객체 지향 언어인 자바에서 관계형 데이터베이스 프로그래밍을 좀 더 쉽게 하도록 도와주는 프레임워크이다.

 

 

 

 

사용 과정

1. MySQL에 테이블을 생성한다.
2. Spring gradle에 Mybatis, Mysql Library를 위한 라이브러리를 작성하고 yml에 mybatis를 위한 설정을 해준다.
3. Mybatis를 활용하기 위해 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="kia.com.mybatistest.model.dao.UserMapper">

    <resultMap id="UserDto" type="kia.com.mybatistest.model.dto.UserDto">
        <result property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="userEmail" column="user_email"/>
        <result property="userPassword" column="user_password"/>
        <result property="createAt" column="create_at"/>
        <result property="modifyAt" column="modify_at"/>
    </resultMap>

    <select id="findByIdAndPassword" resultType="kia.com.mybatistest.model.dto.UserDto">
        SELECT user_email, user_password
        FROM users
        WHERE user_email=#{userEmail}
          AND user_password=#{userPassword}
    </select>

    <select id="saveUser" resultMap="UserDto">
        INSERT INTO users(user_name, user_email, user_password, create_at, modify_at)
        VALUES (#{userName},
                #{userEmail},
                #{userPassword},
                DATE_FORMAT(#{createAt}, '%Y-%m-%d %H:%i:%s'),
                now()
                );
    </select>
</mapper>​

위 코드와 같이 Query문을 작성하고, resultMap을 활용해 객체 클래스와 같은 필드를 가진 Map을 만들어준다.
그리고 여기서 id값은 자바 메소드와 매핑된다.

4. 관련 DTO를 만들어준다.
5. 기능을 수행할 DAO를 만들어준다.

@Repository
@Mapper
public interface UserMapper {
    List<UserDto> getAllUserDataList();
    UserDto saveUser(UserDto userDto);
    UserDto findById(Long id);
    UserDto findByIdAndPassword(LoginUserDto loginUserDto);
}

이와 같이 @Mapper 어노테이션을 통해서 위의 xml 네이밍과 매핑이 되서 서비스 클래스에서 사용할 수 있게된다.

6. 위의 매퍼를 활용해 레포지토리와 서비스를 만들어준다.

 

 

'mybatis' 카테고리의 다른 글

트랜잭션 관리  (0) 2023.09.14
WHERE 절은 왜 OUTER JOIN 에서 데이터를 누락시키는가?  (0) 2023.09.13
mybatis associate, collection, alias  (0) 2023.09.12
Spring + MySQL + Mybatis  (0) 2023.08.17

### mysql 연동 후 dao/dto를 만들 클래스 구성

<br/>

### 쿼리 매퍼를 작성 (ex. UserMapper.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="kia.com.mybatistest.model.dao.UserMapper">
    <select id="getAllUserDataList" resultType="kia.com.mybatistest.model.dto.UserDto">
        select * FROM users
    </select>
</mapper>



- select * FROM {실제 데이터베이스 테이블 명}
- namespace : Mapper interface 위치
- select resultType : Mapping 될 dto 위치



### mybatis-config.xml 작성

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>



### application.yml에 mybatis 설정 추가

mybatis:
  config: mybatis-config.xml // config 위치 : static 바로 아래
  type-aliases-package: kia.com.mybatistest.model // dao,dto가 위치한 곳
  mapper-locations: mybatis/mapper/*.xml // mapper를 위한 xml 파일이 위치한 곳 ( static 아래가 아닌 resources 아래 )




### Mapper interface 작성

@Repository
@Mapper
public interface UserMapper {
    List<UserDto> getAllUserDataList();
}



### Dto 구성

@Getter
@Setter
public class UserDto {
    private Long user_id;
    private String user_name;
    private String user_email;
    private String user_password;
    private LocalDateTime create_at;
    private LocalDateTime modified_at;

    public UserDto() {

    }

    public UserDto(String user_name, String user_email, String user_password, LocalDateTime create_at, LocalDateTime modified_at) {
        this.user_name = user_name;
        this.user_email = user_email;
        this.user_password = user_password;
        this.create_at = create_at;
        this.modified_at = modified_at;
    }

    public UserDto of(String user_name, String user_email, String user_password, LocalDateTime create_at, LocalDateTime modified_at) {
        return new UserDto(user_name, user_email, user_password, create_at, modified_at);
    }
}




### Service interface 작성

public interface UserServiceInterface {

    public List<UserDto> getAllUserDataList();
}



### Service implement 작성

@Service
@RequiredArgsConstructor
public class UserService implements UserServiceInterface {

    private final UserMapper userMapper;

    @Override
    public List<UserDto> getAllUserDataList() {
        return userMapper.getAllUserDataList();
    }
}



### Test Controller 구성

@RequiredArgsConstructor
@RestController
public class MemberTestController {

    private final UserService userService;

    @GetMapping("/user/test")
    public List<UserDto> getAllDataList() {
        return userService.getAllUserDataList();
    }
}

 

 

* github : https://github.com/tjdwns4537/mybatis-practice

 

GitHub - tjdwns4537/mybatis-practice: 실습 및 테스트

실습 및 테스트. Contribute to tjdwns4537/mybatis-practice development by creating an account on GitHub.

github.com

 

[ 에러 발생 코드 ]

public enum StockEnum {

    KAKAO(035720,"카카오"),
    NAVER(035420,"네이버"),
    KIA(000270,"기아"),
    SKENOVATION(096770, "SK이노베이션"),
    LGCHEMISTRY(051910,"LG화학"),
    SAMSUNG(005930,"삼성");

    int number;
    String name;

    StockEnum(int number, String name) {
        this.number = number;
        this.name = name;
    }
}

위 코드를 작성해보면 SK이노베이션의 숫자에 에러가 발생한다.

그 이유는 위 코드는 숫자 0부터 시작하는데, 그 뜻은 java 에서 8진수(base-8) 로 해석한다는 의미입니다.

 

그래서 0 뒤에 있는 8,9에 대해 해석할 수 없으므로 에러가 발생합니다.

 

위 코드를 수정하려면 아래와 같이 String 으로 작성하면 됩니다.

 

 

[ 수정 코드 ]

public enum StockEnum {

    KAKAO("035720","카카오"),
    NAVER("035420","네이버"),
    KIA("000270","기아"),
    SKENOVATION("096770", "SK이노베이션"),
    LGCHEMISTRY("051910","LG화학"),
    SAMSUNG("005930","삼성");

    String number;
    String name;

    StockEnum(String number, String name) {
        this.number = number;
        this.name = name;
    }
}

'JAVA' 카테고리의 다른 글

stream 활용해 list 최소, 최대값 구하기  (0) 2022.11.20
2차원 배열 정렬, 문자열 배열 정렬  (1) 2022.10.03
Queue와 BFS  (1) 2022.09.30
정렬과 lambda  (0) 2022.09.29
PriorityQueue ( 우선순위큐 )  (0) 2022.09.27

에러 :

Caused by: java.io.FileNotFoundException: class path resource [elastic/article-setting.json] cannot be opened because it does not exist

 

말 그래도 경로를 못잡아서 생기는 문제입니다.

 

저 같은 경우,

@Mapping(mappingPath = "elastic/article-mapping.json")
@Setting(settingPath = "elastic/article-setting.json")
public class ArticleDoc {
...
}

이렇게 경로를 잡고 있는데, resource 경로에 파일명도 제대로 되있는데 왜 못잡을까? 를 생각했습니다.

 

정적 파일이 resource/static/elastic/~~.json 과 같이 원래 잡던 경로랑 달라져서 생기는 문제였습니다.

 

아래와 같이 문제를 해결할 수 있습니다.

 

1. WebConfig 설정

package com.example.elasticsearch.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String[] resourceLocation = {
                "classpath:/static/", "classpath:/resources/","classpath:/public/","classpath:/static/elastic/"
        };
        registry.addResourceHandler("/**").addResourceLocations(resourceLocation);
        super.addResourceHandlers(registry);
    }
}

 

 

2. Setting 경로 수정

@Mapping(mappingPath = "/static/elastic/article-mapping.json") // 경로수정
@Setting(settingPath = "/static/elastic/article-setting.json") // 경로수정
public class ArticleDoc {
...
}

 

 

이렇게 하면 문제가 해결되는 것을 볼 수 있습니다.

 

 

!! 참고

https://tjdwns4537.tistory.com/158

 

정적 리소스 사용하기 ( Resource Handler )

상황 : Controller 에서 Templates 경로의 View 를 반환하는데 404가 발생 다음의 상황에서 localhost:8080/testMain 를 접속하면 404가 뜨는 것을 볼 수 있습니다. 이 경우, 다음과 같은 설정을 해줘야 합니다. 위

tjdwns4537.tistory.com

 

 

[ 해결 방법 ] 

1. DP 알고리즘 활용

2. 위치한 값이 1일 때, [ 왼쪽 / 왼쪽 위 대각선 / 위쪽 ] 의 값을 확인해 가장 작은 값에 +1 해준다.

 

 

 

DP 알고리즘

 

- DP 사용 조건

1) 겹치는 부분 문제 : 동일한 작은 문제들이 반복하여 나타나는 경우에 사용이 가능하다.

 

ex. 이진 탐색과 피보나치 수열

이진 탐색 : 정렬된 배열 내에서 그 위치를 바로 찾기 때문에 재사용 과정을 거치지 않아 DP 사용 조건에 해당하지 않는다.

피보나치 수열 : f(n) = f(n-1) + f(n-2) 이므로, 트리구조로 동일한 부분 문제가 중복되어 나타나 DP 사용 조건에 해당한다.

 

2) 최적 부분 구조 : 부분 문제의 최적 결과 값을 사용해 전체 문제의 최적 결과를 낼 수 있는 경우

 

ex. 최적 경로 문제

여러 경로 중 [ A-X, X-B ] 가 가장 짧은 경로라면 A-X-B 가 정답이 된다.
이와 같이 부분 문제의 최적의 결과가 전체 문제에서도 동일하게 적용되어 결과가 변하지 않는 경우 DP를 사용할 수 있다.

 

- DP 사용 과정

1) DP로 풀 수 있는 문제인지 확인

 

2) 변수 간 점화식 만들기

 

3) Memoization

: 변수의 값에 따른 결과를 저장하는 것을 메모라고 부른다. 보통 배열을 사용하며, 1~3차원 배열이 될 수 있다.

 

5) 기저 상태 파악하기

: 가장 작은 문제의 상태를 파악해야 한다.

 

6) 구현하기

Bottom-Up ( 반복문 - Tabulation )
아래에서부터 계산을 수행해 누적시켜 전체 큰 문제를 해결하는 방식

Top-Down ( 재귀 - Memoization )
위에서부터 바로 호출하며 결과 값을 재귀를 통해 전이시켜 재활용하는 방식

 

 

 

[ 문제 코드 ]

 

- Tabulation 방식으로 해결

 

public class 가장큰정사각형찾기 {

    /**
     * TODO
     *  - 목표: 가장 큰 정사각형을 찾아라
     *  - DB 알고리즘 활용
  장  * **/

    public static void main(String[] args) {
        int[][] arr = {{0, 0, 1, 1}, {1,1,1,1}};
        int solution = solution(arr);
        System.out.println(solution);
    }

    public static int solution(int [][]board)
    {
        int answer = 0;

        int[][] map = new int[board.length + 1][board[0].length + 1];

        int maxLen = 0;
        for (int i = 1; i <= board.length; i++) {
            for (int j = 1; j <= board[0].length; j++) {
                if(board[i-1][j-1] != 0) {
                    int min = Math.min(Math.min(map[i - 1][j], map[i][j - 1]), map[i - 1][j - 1]);
                    map[i][j] = min + 1;

                    maxLen = Math.max(maxLen, min + 1);
                }
            }
        }

        return maxLen * maxLen;
    }
}

'프로그래머스' 카테고리의 다른 글

가장 큰 수  (0) 2023.03.25
다음 큰 숫자  (1) 2022.11.20
124 나라의 숫자  (0) 2022.11.20
[1차] 캐시  (0) 2022.11.17
최솟값 만들기  (0) 2022.10.26

[ 문제 해결1 ]

각 배열에 원소의 자릿 수를 비교해서 정렬해보니 시간이 오버된다는 것을 알 수 있었습니다.

 

 

[ 문제 해결2 ]

Comparator 을 사용해 문자열을 붙쳐서 판단 후, 내림차 순 해본다.

compareTo method 는 앞에서부터 비교하고 다른 문자열이 나오면 'a-b' 순서로 문자의 아스키 코드 값을 뺀 결과를 리턴합니다.

 

 

* 내림차순 : (o2+o1).compareTo(o1+o2);

* 오름차순 : (o1+o2).compareTo(o1+o2);

 

 

[ 코드 ]

import java.util.*;

public class 가장큰수 {

    /**
     *  TODO
     *      - 문제: 주어진 정수에서 가장 큰 수를 만들어라
     *      - 정수 배열을 문자 배열로 변환
     *      - Comparator 을 활용해 내림차 순 정렬
     *      - compareTo 의 아스키 코드 값 비교를 이용
     * **/

    public static void main(String[] args) {
        int[] arr = {3, 30, 34, 5, 9};
        String solution = solution(arr);
        System.out.println(solution);

    }

    public static String solution(int[] numbers) {
        String answer = "";

        String[] arr = new String[numbers.length];

        for (int i = 0; i < arr.length; i++) {
            arr[i] = String.valueOf(numbers[i]);
        }

        Arrays.sort(arr, new Comparator<String>() { // 내림차순 정렬
            @Override
            public int compare(String o1, String o2) {
                return (o2 + o1).compareTo(o1 + o2);
            }
        });

        if(arr[0].equals("0")) return "0";

        for (String i : arr) {
            answer += i;
        }

        return answer;
    }
}

 

 

 

[ 개념 ]

- Comparator 이 무엇인가?

기본 정렬이 아닌 다른 기준으로 사용하고 싶을 때 사용하는 인터페이스입니다.

그 중 compare를 구현함으로써 임의의 클래스에 대해 정렬 기준을 만들 수 있습니다.

compare override method

- 첫 번째 인자 < 두 번째 인자 : 음수
- 첫 번째 인자 > 두 번째 인자 : 양수
- 첫 번째 인자 == 두 번째 인자 : 0

 

그래서 Collections.sort 에 comparator 를 인자로 사용하는 부분을 살펴보겠습니다.

 

인자에는 다음과 같은 항목이 들어갈 수 있습니다.

 

1. 첫번째 인자 : 서로 다른 클래스에 존재하는 리스트

2. 두번째 인자 : Comparator 인터페이스의 compare 함수를 오버라이딩한 클래스

 

이렇게 사용하면 사용자가 원하는 형태로 정렬을 구현할 수 있게 됩니다.

'프로그래머스' 카테고리의 다른 글

가장 큰 정사각형 찾기 ( DP 알고리즘 )  (0) 2023.03.26
다음 큰 숫자  (1) 2022.11.20
124 나라의 숫자  (0) 2022.11.20
[1차] 캐시  (0) 2022.11.17
최솟값 만들기  (0) 2022.10.26

서블릿

웹 서버 프로그래밍을 하기 위한 자바 코드라고 할 수 있습니다.

 

  • 서블릿의 형태 : HttpServlet 클래스를 상속한 클래스
  • 서블릿의 관리 : Servlet Container 에 의해 관리,실행됩니다.

 

개발자는 서블릿을 가지고 무엇을 하는가?
  • 웹 서버 역할 : HTTP Server + Servlet Container 를 통해 웹 서버 역할에 필요한 부분을 구현
  • 개발자의 역할 : 서블릿을 만들어 HTTP 요청을 처리하고 받는 부분을 구현

 

 

대표적인 웹서버 : Tomcat

웹 애플리케이션 서버 ( WAS ) 중 하나인 톰캣은 개발자가 작성한 서블릿을 관리해주는 Servlet Container 입니다.

 

  • 여기서 "서블릿을 관리" 해준다는 의미는?

클라이언트가 어떠한 요청을 했을 때, 어떤 서블릿을 실행할 것인지 제어해주는 것을 의미합니다.

 

  • Tomcat 의 주요 구성인 web.xml 이란?

Servlet에 대한 정보를 쓰는 파일입니다. 최근에는 대부분 java config 파일로 자바 소스 설정을 합니다.

 

  • 서버에서 수행하는 작업

1. 서버 TCP/IP 연결

2. HTTP 요청 메시지 파싱해서 읽기

3. POST 방식으로 URL 읽기

4. Content-Type 확인

5. HTTP Message-body 내용 파싱

6. 비지니스 로직 실행

7. HTTP Response Message 생성

8. TCP/IP에 응답 후 소켓 종료

 

위의 1~8번 중 6번을 제외한 부분을 WAS 가 대신 해줍니다.

 

 

 

대표적인 서블릿

1. DispatcherServlet

 - HTTP 프로토콜로 들어오는 모든 요청을 받아, 적합한 컨트롤러에 위임해주는 프론트 컨트롤러

 - 처리 과정

: 클라이언트의 요청 -> 서블릿 컨테이너가 요청 받음 ( ex. 톰캣 ) ->

  DispatcherServlet -> [ 공통적인 작업을 먼저 처리한 후 해당 요청을 처리해야 하는 컨트롤러를 찾아 작업 위임 ]

 

2. Servlet Filter

- 서블릿 실행 전, 후에 어떤 작업을 하고자 할 때 사용됩니다.

 

3. Servlet Context

- 서블릿 단위로 생성되는 컨텍스트입니다.

- DispatcherServlet 과 같은 서블릿을 등록하면 해당 서블릿이 갖는 하나의 작은 컨테이너 역할을 하는 객체

 

4. Application Context

- Root Context로 스프링에 의해 생성되는 Bean에 대한 Spring IoC Container  입니다.

- BeanFactory 를 상속받는 Context

- 여러 서블릿에서 공통으로 사용할 Bean을 등록하는 Context

 

5. WebApplicationInitializer

- Servlet Context 를 프로그래밍적으로 설정하기 위한 인터페이스 ( web.xml 을 대체하기 위함 )

 

6. ContextLoaderListener

- 서블릿 컨테이너의 시작과 종료시에 발생하는 이벤트를 처리하는 리스너를 등록하기 위한 

   ServletContextListener 인터페이스의 구현체


7. AnnotationConfigWebApplicationContext

- Component  class( @Configuration, @Inject, @Component ) 를 입력값으로 받는

WebApplicationContext 인터페이스의 구현체

 

서블릿의 형태

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class helloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("helloServlet.service");
        System.out.println("request = " + request);
        System.out.println("response = " + response);

        String username = request.getParameter("username");
        System.out.println("username : " + username);

        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello" + username);

    }
}

- 동작 

1. "/hello" 라는 url 이 들어오면, service에 있는 코드를 실행시켜 줍니다.

2. HTTP 요청 정보 사용하기 : HttpServletRequest

3. HTTP 요청 정보 제공하기 : HttpServletResponse ( 개발자들은 여기에 데이터를 넣어준다. )

4. requset.getParameter('username") 

 : hello?username=park 했을 때, 입력되는 park 를 받아온다.

 

5. reponse에 응답 코드를 작성

6. 포스트맨으로 테스트

 

 

로그인 동작 예시
<form action="/request-param" method="post">
    username: <input type="text" name="username" />
    password: <input type="password" name="password" /> <button type="submit"> 전송 </button>
</form>

이러한 Form 형식이 있을 때,

버튼을 누르면 "/requset-param" 이라는 url 로 POST 를 보내게 됩니다.

 

@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("단일 파라미터 조회 - start");
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        System.out.println("username = " + username);
        System.out.println("password = " + password);
        System.out.println("단일 파라미터 조회 - end");

        if(username.equals("kim") && password.equals("1234")){
            response.getWriter().write("ok");
        }
    }
}

- 결과 : ok 가 출력되며 정상 동작함을 알 수 있습니다.

 

 

위의 서블릿으로 구현한 코드를 Spring MVC 로 구현한다면?

 

1. HttpServletRequest 대신 @RequestParam

위의  service 코드를 controller에 @RequestParam 을 통해 간단하게 나타낼 수 있습니다.

@PostMapping("/save")
public String process(
            @RequestParam String username,
            @RequestParam String password,
            Model model
    ) {
        Member member = new Member(username, password);
        memberRepository.save(member);

        model.addAttribute("member", member);
        return "save-result";
}

 

2. 타임리프를 활용한 동적인 뷰 템플릿 구성

서블릿에 작성한 HTML 코드는 정적인 코드로 여러 문제점을 가지고 있습니다.

 

[ 기존코드 ]

@WebServlet(name = "memberSaveServlet", urlPatterns = "/save")
public class memberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void  service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MemberSaveServlet.service");

        // 요청 데이터
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 저장
        Member member = new Member(username, password);
        memberRepository.save(member);

        // 응답 메시지
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" + "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                "    <li>id="+member.getId()+"</li>\n" +
                "    <li>username="+member.getUsername()+"</li>\n" +
                " <li>password="+member.getPassword()+"</li>\n" + "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
                "</html>");
    }
}

 

위의 write 부분에 작성한 html 을 아래의 타임리프를 활용한 코드로 정적으로 사용자 데이터를 관리할 수 있습니다.

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<main class="form-signin">
  <form th:action="${/member/save}" method="post">
    <h1 class="h3 mb-3 fw-normal">Sign in</h1>
    <div class="form-floating">
      <input type="text" class="form-control" id="floatingInput" name="username" placeholder="아이디를 입력하세요.">
      <label for="floatingInput">ID</label>
    </div>
    <div class="form-floating">
      <input type="password" class="form-control" id="floatingPassword" name="password" placeholder="비밀번호를 입력하세요.">
      <label for="floatingPassword">Password</label>
    </div>

    <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
  </form>
</main>
  </body>
</html>

타임리프의 action 코드를 통해 POST 로 사용자가 정보를 입력 후 버튼을 누르면

POST로 "member/save" 의 url 에 사용자 정보를 보냅니다.

그러면 위에 작성한 POST의 process 메서드로 비지니스 로직을 구현할 수 있게 됩니다.

 

이를 통해 기존에 서블릿 부분에 작성한 코드는 작성할 필요가 없게 됩니다.

'스터디' 카테고리의 다른 글

Swagger Annotation  (0) 2023.09.13
CSRF  (0) 2022.12.31
세션과 쿠키, JWT  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01

CSRF

Cross Site Request Forgery 로,

정상적인 사용자가 의도치 않은 위조 요청을 보내는 것을 의미합니다.

 

그래서 CSRF protection 은 Spring Security 에서 디폴트로 설정됩니다.

protection 을 통해 GET 요청을 제외한 상태를 변화시킬 수 있는 POST,PUT,DELETE 요청으로부터 보호하게 됩니다.

 

CSRF 취약점

사용자가 의도하지 않은 요청을 수행하게 하는 취약점을 가지고 있습니다.

예를 들어, 로그인한 사용자 권한을 사용하여 다른 페이지에서 패스워드를 변경하게 되는 경우입니다.

 

 

 

CSRF 를 왜 disable 하기 시작하였는가?

Spring Security Documentation 에 non-broswer clients 만을 위한 서비스라면 disalbe 해도 좋다고 합니다.

 

그 이유는 REST API 를 이용한 서버라면 세션 기반 인증과는 다르게 stateless 하기 때문에

사용자의 정보를 세션에 저장하지 않아, 서버에 인증정보가 따로 보관되어 있지 않습니다.

일반적으로 JWT 같은 토큰을 사용해 인증하기 때문에, 토큰을 쿠키에 저장하지 않는다면 CSRF 취약점에 대해서

어느정도 안전하다고 말할 수 있습니다.

 

 

 

결론

Spring Security 는 디폴트로 헤더에 Cache-Control 을 추가하고 no-cache, no-store 등의 옵션을 추가합니다.

이는 브라우저에게 쿠키를 못쓴다고 알려주는 역할을 합니다.

그래서 쿠키를 사용하려면 Cache-Control 이라는 헤더를 없애줘야합니다.

 

그리고 쿠키를 쓰는 방식은 보안에 취약하여 크롬에서 쿠키없이 보안 설정을 하려고 노력하고 있다고 합니다.

따라서 Spring Security도 이에 발맞추고 있으므로 csrf 공격에 대비하기 위하여 csrf 토큰을 만들 필요가 없습니다.

 

 

 

사용예시

http
                .formLogin()
                .loginPage("/member/login")
                .loginProcessingUrl("/member/login-do")
                .defaultSuccessUrl("/main")

위의 코드로 로그인을 구현한다면 /login-do 로 POST 를 사용하여 로그인 기능을 처리하고 싶지만, POST는 되지 않습니다.

그래서 다음 코드를 추가해줘야 합니다.

.and()
                .csrf().disable();

CSRF 기능을 끈다면 POST 를 통해 사용자 정보를 사용할 수 있게 됩니다.

'스터디' 카테고리의 다른 글

Swagger Annotation  (0) 2023.09.13
서블릿과 스프링MVC  (0) 2023.01.01
세션과 쿠키, JWT  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01

세션과 쿠키는 왜 필요한가?

HTTP 프로토콜의 약점을 보완하기 위해 사용됩니다.

 

 * HTTP의 비연결성과 무상태

1) 비연결성
: HTTP는 인터넷상에서 불특정 다수의 통신 환경을 기반으로 설계되었습니다.
만약, 서버에서 다수의 클라이언트와 계속 연결을 유지한다면 많은 리소스가 발생합니다.
그래서 서버가 한 번 연결을 맺은 후,
클라이언트 요청에 대해 서버가 응답을 마치면 맺었던 연결을 끊어버리는 성질을 가지게 되는데
이를 비연결성이라고 부릅니다.
하지만 이러한 비연결성은 모든 요청에 대해 매번 새로운 연결을 시도하므로 연결/해제에 대한 오버헤드를 가집니다.

2) 무상태
서버가 클라이언트의 상태를 보존하지 않음으로 클라이언트의 상태를 알 수 없는 상태입니다.

 

이러한 무상태는 로그인이 필요없는 단순한 서비스 소개 화면 정도의 구현에는 유용합니다.

하지만 사용자가 로그인한 상태를 서버에 유지 시켜 줘야 하는 경우 무상태로는 설계를 할 수 없게 됩니다.

이를 위해 세션과 쿠키를 조합하여 상태를 유지합니다.

 

Cookie

클라이언트 로컬에 저장되는 키와 값이 들어있는 작은 데이터 파일

  • 사용자 인증이 유효한 시간을 명시할 수 있음
  • 유효 시간이 정해지면 브라우저가 종료되어도 인증이 유지됨
  • Response Header 에 Set-Cookie 속성을 사용해 클라이언트에 쿠키를 만들 수 있음

 

Cookie 구성요소

  • 이름 : 쿠키를 구별하는데 사용되는 이름
  • 값 : 쿠키의 이름과 관련된 값
  • 유효시간 : 쿠키 유지시간
  • 도메인 : 쿠키를 전송할 도메인
  • 경로 : 쿠키를 전송할 요청 경로

 

Cookie 동작방식

  • 클라이언트가 페이지를 요청
  • 서버에서 쿠키 생성
  • HTTP 헤더에 쿠키를 포함시켜 응답
  • 브라우저가 종료되어도 쿠키 만료 시간이 있다면 클라이언트에서 보관함
  • 같은 요청을 할 경우 HTTP 헤더에 쿠키를 함께 보냄
  • 서버에서 쿠키를 읽어 이전 상태 변경할 필요가 있을 때 쿠키를 업데이트

 

Cookie 사용 예시

  • 방문 사이트에서 "아이디,비번 저장하시겠습니까?"
  • 쇼핑몰 장바구니 기능
  • 자동로그인, 팝업에서 "오늘 더 이상 이 창 보지 않음" 체크

 

Session

브라우저와 웹 서버가 연결되어 브라우저가 종료될때까지의 시점

  • 세션은 쿠키를 기반으로 하지만, 사용자 정보 파일을 브라우저에 저장하는 쿠키와 달리 서버 측에서 관리
  • 서버는 클라이언트 구분을 위해 세션 ID를 부여하며, 웹 브라우저가 서버에 접속해 브라우저를 종료할 때가지 인증상태 유지
  • 접속 시간에 제한을 두어 일정 시간 응답 없을 경우 유지되지 않게 설정 가능
  • 사용자에 대한 정보를 서버에 두기 때문에 쿠키보다 보안에 좋음
  • 하지만 사용자가 많아질수록 서버 메모리를 많이 차지함
  • 즉, 동접자 수가 많은 경우 웹 서버에 과부하를 주게 됨

 

 

Session 동작 방식

1. 클라이언트가 브라우저를 통해 서버에 접속한다.

2. 서버는 세션id를 쿠키에 담아 되돌려준다.

3. 클라이언트는 세션id를 담은 쿠키인 세션 쿠키를 이후 요청부터 계속해서 전달한다.

이를 세션 기반 인증 방식이라고 하며, 간단하게 세션이라고 부른다.

 

Session 사용 예시

  • 로그인 같이 보안상 중요한 작업 수행할 때 사용

 

JWT

JSON 객체를 사용해서 토큰 자체에 정보들을 저장하고 있는 Web Token

세션은 사용자 수 만큼 서버 메모리를 차지하기 때문에, 최근에는 이러한 토큰 기반 인증 방식을 사용한다.

 

 

JWT 구성

- Header

Signature 를 해싱하기 위한 알고리즘 정보들이 담겨져 있음

 

- Payload

서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보에 대한 내용을 담고 있습니다.

여기에 담는 정보의 "한 조각" 을 클레임이라고 부릅니다. 이 클레임은 토큰에 여러개가 들어갈 수 있습니다.

클레임은 name/value 한 쌍으로 이뤄져 있습니다.

 

ex. 클레임 토큰 예시

{
    "id" : "tjdwns",
    "role" : "user",
    "email" : "tjdwns4537@email.com"
}

 

- Signature

토큰의 유효성 검증을 위한 문자열로, 이 문자열을 통해 서버에서는

이 토큰이 유효한 토큰인지 아닌지를 검증할 수 있음

 

 

JWT 장점

- 중앙의 인증서버, 데이터 스토어에 대한 의존성 없음, 시스템 수평적 확장에 유리

- Base64 URL Safe Encoding -> URL, Cookie, Header 모두 사용 가능

 

 

JWT 단점

- PayLoad 정보가 많아지면 네트워크 사용량 증가, 데이터 설계 고려 필요

- 토큰이 클라이언트에 저장되기 때문에, 서버에선 클라이언트의 토큰을 조작할 수 없게 됩니다.

 

 

JWT  사용 과정

1. 유저가 로그인

2. 유저의 정보를 기반으로 토큰을 발급해 유저에게 전달

3. 유저가 서버에 요청을 할때마다 JWT를 포함하여 전달

4. 서버가 클라이언트에게서 요청을 받을때마다, 토큰이 인증가능한지 검증

5. 유저가 요청한 작업에 권한이 있는지 확인하여 작업 처리

 

 

JWT 를 왜 사용하는가?

쉽게 말해 JWT는 일종의 확인서이다.

우리가 웹사이트에 로그인을 하여 Authentication이 이뤄지면, 서버는 사인된 JWT를 우리에게 제공한다.

그러면 앞으로 요청할 때 마다 JWT를 서버에게 같이 보여주면서 권한을 확인받는 것이다.

서버는 JWT만 확인해 Authentication하기 때문에 세션DB에 저장할 필요가 없다.

즉, JWT를 서버에 발급할 때, 검증할 때만 확인서를 다루기 때문에 서버 자원을 효과적으로 절감할 수 있다.

 

 

 

 

 

 

 

 

'스터디' 카테고리의 다른 글

서블릿과 스프링MVC  (0) 2023.01.01
CSRF  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01
logging 알아보기  (0) 2022.11.30

@Valid  로 검증을 할 때 발생하는 에러

 

생각보다 간단한 에러입니다.

 

[ 문제 상황 ]

1. 회원가입 페이지의 Domain에 이름과 연락처 등에 대해 @NotEmpty 을 붙쳐줬습니다.

2. 컨트럴러에서 @Valid, BindingResult 피라미터를 넣어줍니다.

3. 검증에러가 발생시 bindingResult 객체에 에러가 담기고, 에러가 발생하면 다시 재입력받을 수 있는 페이지를 구축하는 것입니다.

 

* 발생한 문제: 검증이 발생하는 것은 파악이 되지만, bindingResult 객체가 제대로 작동하지 않음

 

 

 

 

[ 문제 해결 ]

검증은 피라미터 순서에 영향을 받습니다.

따라서

@PostMapping("/join")
    public String joinExecute(
            @Valid MemberForm memberForm,
            @RequestParam String password2,
            RedirectAttributes redirectAttributes,
            Model model,BindingResult bindingResult)

위의 코드처럼 @Valid 후 BindingResult 순서가 바로 붙어있는게 아닌 저렇게 다른 피라미터를 먼저 받을 경우 원하는 동작을 안합니다.

 

@PostMapping("/join")
    public String joinExecute(
            @Valid MemberForm memberForm,BindingResult bindingResult,
            @RequestParam String password2,
            RedirectAttributes redirectAttributes,
            Model model)

이렇게 피라미터 순서를 제대로 배치하면 해결됩니다.

해당 오류의 문제

: H2 Database 연결에 대한 문제입니다.

저 같은 경우

jdbc:h2:tcp://localhost/~/random

이와 같이 random 이라는 데이터베이스를 새로 만들었는데

정작 데이터베이스를 만들지 않았기 때문에 발생한 문제입니다.

 

최근 H2 Database 가 보안적에 관한 규칙이 생겨 자동으로 데이터베이스를 생성하지 않아 생기는 문제입니다.

 

 

[ 해결방법 ]

 

ㅁㅁ

위의 드래그된 곳에 자신이 만들 데이터베이스을 작성하면 됩니다.

 

접속은

 jdbc:h2:tcp://localhost/./random

 

이와 같이 접속하면 됩니다.

 

발생하고 있던 문제 :

Button type = "submit" 으로 버튼 이벤트를 주다보니 다른 페이지로 이동해버리는 문제가 발생

 

 

[ 해결 완료 ]

 

이전 블로그에서는 다른 팝업창을 띄워서 그 곳에서 인증하는 방식으로 해결한다고 말했었습니다.

하지만 더 간단하고 쉬운 방법을 찾았습니다.

 

<form th:action="@{/member/emailConfirm}" method="post" target="param">
            <div>
                <h3 class="join_title"><label for="email">이메일</label></h3>
                <span class="box int_email">
                        <input type="text" name="email" id="email"
                               class="form-control" maxlength="100">
                    </span>
            </div>

            <!-- 이메일 인증 번호 요청 버튼 -->
            <input type="submit"
                   value="인증번호받기"
                    id="email-btn"
            />
</form>
<iframe id="if" name="param"></iframe>

위와 같이 iframe 을 활용하여 간단하게 해결할 수 있었습니다.

 

iframe 에 대한 설명은 아래의 글이 잘 적혀있어 읽어보시고 활용하면 좋을것 같습니다.

 

https://yeoulcoding.me/143

 

[HTML] iframe 태그란?

Definition iframe이란 Inline Frame 의 약자로, 웹 브라우저 내에 또 다른 프레임, 즉 현재 브라우저에 렌더링되고 있는 문서 안에 또 다른 HTML페이지를 삽입할 수 있도록 하는 기능을 제공합니다. 검색

yeoulcoding.me

 

 

만들려는 기능은 다음과 같습니다.

 

  1. 이메일 입력
  2. 인증번호 요청 클릭
  3. tjdwns4537@naver.com 이메일에 인증번호 전송
  4. 전송된 인증번호를 입력창에 입력 후 정상적으로 인증됬는지 확인

 

[ Controller ]

@PostMapping("/emailConfirm")
@ResponseBody
public void emailVerify(@RequestParam String email) throws Exception {
        log.info("post email = {}", email);
        emailService.sendSimpleMessage(email);
}

 

[ EmailService ]

package smilegate.securitySystem.service.EmailService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Random;

@Service
public class EmailServiceImp implements EmailServiceInterface{

    @Autowired
    JavaMailSender emailSender;

    public static final String emailPassword = createKey();

    public EmailServiceImp(JavaMailSender emailSender) {
        this.emailSender = emailSender;
    }

    public String getEmailPassword() {
        return emailPassword;
    }

    public static String createKey() {
        StringBuffer key = new StringBuffer();
        Random rnd = new Random();

        for (int i = 0; i < 8; i++) { // 인증코드 8자리
            int index = rnd.nextInt(3); // 0~2 까지 랜덤

            switch (index) {
                case 0:
                    key.append((char) ((int) (rnd.nextInt(26)) + 97));
                    //  a~z  (ex. 1+97=98 => (char)98 = 'b')
                    break;
                case 1:
                    key.append((char) ((int) (rnd.nextInt(26)) + 65));
                    //  A~Z
                    break;
                case 2:
                    key.append((rnd.nextInt(10)));
                    // 0~9
                    break;
            }
        }
        return key.toString();
    }

    @Override
    public String sendSimpleMessage(String to)throws Exception {
        // TODO Auto-generated method stub
        MimeMessage message = createMessage(to);
        try{//예외처리
            emailSender.send(message);
        }catch(MailException es){
            es.printStackTrace();
            throw new IllegalArgumentException();
        }
        return emailPassword;
    }

    private MimeMessage createMessage(String to)throws Exception{
        System.out.println("보내는 대상 : "+ to);
        System.out.println("인증 번호 : "+ emailPassword);
        MimeMessage message = emailSender.createMimeMessage();

        message.addRecipients(MimeMessage.RecipientType.TO, to);//보내는 대상
        message.setSubject("회원가입 이메일 인증");//제목

        String msgg="";
        msgg+= "<div style='margin:100px;'>";
        msgg+= "<h1> 안녕하세요 Sungjun 입니다. </h1>";
        msgg+= "<br>";
        msgg+= "<p>아래 코드를 회원가입 창으로 돌아가 입력해주세요<p>";
        msgg+= "<br>";
        msgg+= "<p>감사합니다!<p>";
        msgg+= "<br>";
        msgg+= "<div align='center' style='border:1px solid black; font-family:verdana';>";
        msgg+= "<h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>";
        msgg+= "<div style='font-size:130%'>";
        msgg+= "CODE : <strong>";
        msgg+= emailPassword+"</strong><div><br/> ";
        msgg+= "</div>";
        message.setText(msgg, "utf-8", "html");//내용
        message.setFrom(new InternetAddress("kktd4537@gmail.com","SungJun"));//보내는 사람

        return message;
    }
}

 

[ Form 태그 ]

 

<form th:action="@{/member/emailConfirm}" method="post">
            <div>
                <h3 class="join_title"><label for="email">이메일</label></h3>
                <span class="box int_email">
                        <input type="text" name="email" id="email"
                               class="form-control" maxlength="100">
                    </span>
            </div>

            <button type="submit"
                    id="email-btn"> 인증번호 요청
            </button>
        </form>

        <div>
            <h3 class="join_title">인증 번호</h3>
            <input type="text" name="email-check" id="email-check" class="form-control" maxlength="100" placeholder="인증번호 입력">
        </div>

 

메일은 정상적으로 보내지는게 확인이 되지만

위의 코드에서 문제점이 있습니다.

 

코드를 보면 Form action 태그를 통해 member/emailConfirm 으로 url 을 요청하기 때문입니다.

 

그렇다고 회원가입 페이지인 member/join 에 다시 보내게 된다면 어떻게 될까요?

email 인증이 아닌 회원가입 버튼의 주소가 member/join 으로 되있기 때문에 PostMapping 메서드 두 개가

서로 같은 곳을 경로를 바라보게 됩니다.

 

그래서 생각한 해결방안은 다음과 같습니다.

 

[ 해결방안1 ]

1) 인증번호 요청 버튼 클릭

2) 자바스크립트를 이용해 팝업창 생성

3) 팝업창에서 인증번호에 대한 검증을 진행

4) 검증이 정상적으로 완료된다면 회원가입 페이지에 검증 완료 메세지 전송

 

[ 해결방안2 ]

1) 인증번호 요청 버튼 클릭

2) RestController 에 구현한 다른 PostMapping 메서드를 호출

3) 해당 메서드에서 검증기능 구현 후 검증이 완료 된다면 회원가입 페이지에 검증 완료 메세지 전송

 

사실 해결방안1에 대한 설계는 머릿속에 바로 들어오지만,

해결방안2에 대한 구조는 저도 어떻게해야 할지 잘 생각이 나지 않습니다.

 

그래서 해결방안1에 대해 먼저 진행해보도록 하겠습니다.

 

로그인 페이지에 대한 컨트롤러 구현 방법

로그인 페이지를 구현할 때, 생각해보면

 

1) 로그인 페이지를 띄우는 것은 GET 으로만 띄우면 될텐데, 정보를 보낼 때 POST로 보내면 되는 것인가?

2) 그렇다면 그걸 어떻게 구현하면 될까?

 

이 두가지에 대한 의문점이 있습니다.

 

 

GET 으로 로그인 페이지를 띄우고, 로그인 버튼 누르면 POST로 보내기

[ controller ]

@GetMapping("/join/login")
public String login() {
     return "/join/login";
}

 

[ login.html ]

<form th:action="/join/login" method="post">
    id: <input type="text" name="username" />
    Password: <input type="text" name="password" /> <button type="submit"> 전송 </button>
</form>

위의 HTML Form 태그와 컨트롤러를 구현하면 됩니다.

 

url : localhost:8080/join/login

을 접속하면 로그인 화면이 보입니다.

 

로그인에 아이디와 비번 입력 후 로그인 버튼을 누르면, /join/login 으로 POST 방식으로 데이터를 전송합니다.

 

[ controller ]

@PostMapping("/join/login")
public String register(Model model) {
    return "/join/login";
}

그러면 /join/login 주소를 POST 로 접속하며, register 메서드가 실행됩니다.

 

즉, 같은 url 이지만 GET 이냐, POST 이냐에 따른 HTTP 메서드에 따라 구분하게 되는 것입니다.

 

 

'스터디' 카테고리의 다른 글

CSRF  (0) 2022.12.31
세션과 쿠키, JWT  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01
logging 알아보기  (0) 2022.11.30
Spring MVC  (0) 2022.11.30

[ 구현 과정 ]

1. 컨트롤러 / 도메인/ 레포지토리 생성

2. 타임리프를 활용해 동적 HTML 생성

 

 

컨트롤러와 타임리프의 실행 과정
package springmvc.springmvc.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import springmvc.springmvc.domain.Member;
import springmvc.springmvc.repository.MemberRepositoryInterface;

import javax.annotation.PostConstruct;
import java.util.List;

@Slf4j
@Controller
@RequestMapping("/join")
@RequiredArgsConstructor
public class joinController {

    private final MemberRepositoryInterface memberRepository;

    @GetMapping("/memberView")
    public String memberListView(Model model) { // 회원 목록 조회
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        log.info("info log={}",model.getAttribute("members"));

        return "/join/memberView";
    }

    @PostConstruct
    public void init() { // 테스트용 멤버 데이터 넣기
        memberRepository.save(new Member("성준","tjdwns", "1234", 010));
        memberRepository.save(new Member("수진","sujin", "2345", 011));
        memberRepository.save(new Member("성형","tjdgud", "0101", 533));
    }

    @GetMapping("/memberView/{id}")
    public String item(@PathVariable Long id, Model model) {
        Member findMember = memberRepository.findById(id);
        model.addAttribute("member", findMember);
        return "/join/member";
    }
}

 

@RequestMapping("/join")

회원가입과 관련된 HTTP url은 모두 join/ 로 시작하기때문에 join을 넣어놨습니다.

 

 

@PostConstruct
    public void init() { // 테스트용 멤버 데이터 넣기
        memberRepository.save(new Member("성준","tjdwns", "1234", 010));
        memberRepository.save(new Member("수진","sujin", "2345", 011));
        memberRepository.save(new Member("성형","tjdgud", "0101", 533));
}

테스트를 위해 프로젝트 생성시 멤버 리포지토리에 위의 멤버를 넣습니다.

 

 

@GetMapping("/memberView")
    public String memberListView(Model model) { // 회원 목록 조회
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        log.info("info log={}",model.getAttribute("members"));

        return "/join/memberView";
}

1) /join/memberView 라는 url 을 접속

2) 멤버 리포지토리에서 findAll을 통해 회원 리스트를 생성

3) 모델에 members 라는 이름으로 회원 리스트를 저장

 

 

<tr th:each="member : ${members}">
                <td><a href="member.html" th:href="@{/join/memberView/{id}(id=${member.id})}" th:text="${member.id}">회원 식별번호</a></td>
                <td th:text="${member.memberName}">이름</a></td>
                <td th:text="${member.memberId}">아이디</a></td>
                <td th:text="${member.phoneNumber}">휴대폰 번호</td>
                <td th:text="${member.memberPassword}">password</td>
 </tr>

1) 타임리프를 활용해 모델에 담겨져있는 members 리스트를 순환

2) 각각의 리스트 목록에 [ 클래스.멤버 ] 로 모델에 담긴 데이터를 출력합니다.

3)

th:href="@{/join/memberView/{id}(id=${member.id})}" th:text="${member.id}"

/join/memberView 라는 브라우저 Url 에서 member.id 로 받아온 회원 식별번호를 뒤에 붙쳐서

/join/memberView/회원식별번호 로 링크를 걸어줍니다.

 

 

 

[ 출력 화면 ]

위의 화면에서 식별 번호에 링크를 누르면

 

회원 정보가 출력됩니다.

위와 같은 에러가 발생한 이유는 Database 에 연결할 필요 정보가 없기 때문입니다.

 

[ 해결방법 ]

 

위의 에러는 appllication.properties 에 데이터베이스에 대한 설정을 해줘야합니다. 아래 블로깅을 보시면 보실 수 있습니다.

 

https://tjdwns4537.tistory.com/117

 

Spring Boot 와 JPA( MySQL ) 연동 간단 가이드

* JPA 와 스프링 부트로 H2Database 를 사용해봤다는 가정하에 작성하였습니다. 1. 의존성 추가 org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java 2. application.properties 설정 추가 server.address=

tjdwns4537.tistory.com

 

 

그런데 여기서 의문점은 저는 jpa 와 데이터베이스를 사용하지 않고 순수 자바로만 레포지토리를 구현했는데 왜 이러한 에러가 뜰까?

였습니다.

 

단순히 강의를 따라하다보니 발생한 문제라고 생각하여 그 원인을 찾기로 하였습니다.

 

[ 의문점 해결방법 ]

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

@Data  를 붙친다는게 실수로

@Entity 를 넣었기 때문입니다.

 

엔티티 어노테이션을 붙치면서 라이브러리에 위의 jpa 라이브러리를 무의식 중에 적용시켰습니다...

그래서 jpa 관련 설정을 찾는 에러가 발생했습니다.

위의 라이브러리를 지워우고 다시 적용시켜주면 에러가 해결됩니다.

 

로그인 페이지는 GET / POST 어떤 메서드를 사용해야 할까?

로그인 페이지를 구현하는 중에 다음과 같은 궁금한 점이 생겼습니다.

 

[ 로그인 흐름 ]

1) 로그인 페이지

2) 아이디 / 비밀번호를 요청 ( 로그인 요청 )

3) 사용자 식별 응답 받음

4) 메인 페이지 진입

 

[ 의문점 ]

여기서 로그인 요청은

1. URL 에 아이디 / 비밀번호를 담아 GET 요청을 해야할지

2. Request Body 에 아이디와 비밀번호를 담아 POST 요청을 해야할지

 

에 대한 의문이 생겼습니다.

 

[ 해결 ]

 

간단하게 결론부터 말하면, 원래라면 HTTPS 를 사용해야합니다.

그런데 만약 HTTPS 를 사용하지 않는다면 POST 를 사용해야합니다.

 


HTTPS 사용해야하는 이유

HTTP 는 텍스트 교환이므로 누군가 네트워크에서 신호 가로채면 내용이 노출되는 보안 이슈가 존재합니다.

 

HTTP 의 동작 과정

TCP -> HTTP

 

HTTPS 동작 과정

TCP -> SSL -> HTTP

 

모든 사이트를 HTTPS 로 하지 않는 이유

HTTP 통신하는 소켓 부분을 SSL / TLS 라는 프로토콜로 대체합니다.

그래서 암호화 과정이 거치므로 속도 저하가 발생하기 때문입니다.


POST 를 사용하는 이유

GET 요청은 서버 데이터의 상태를 변경하지 않기 때문에 쿼리를 캐싱할 수 있어

아이디와 비밀번호와 같은 정보를 쿼리에 담아 보내게 되면 보안에 취약해집니다.

 

ex. 다음 페이지가 로딩되거나 이동할 때 텍스트가 표시됨

 

반면 POST 요청은 서버 상태를 변경시키고 동일한 응답을 주며 서버를 동일한 상태로 유지하기 위해

캐싱을 하지 않기 때문에 GET 보다는 보안에 용이합니다.

 

ex. 5번의 로그인에 실패했을 때 6번째 요청에서 IP 차단에 대한 응답을 할 수 있음

'스터디' 카테고리의 다른 글

세션과 쿠키, JWT  (0) 2022.12.31
로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
logging 알아보기  (0) 2022.11.30
Spring MVC  (0) 2022.11.30
MVC 패턴  (0) 2022.11.28

실무에서는 System.out.print() 가 아닌 로그로 출력 흐름을 파악합니다.

 

위의 코드를 보면,

 

1) RestController

: 일반 컨트롤러는 반환 값이 스트링이면 뷰 이름으로 인식됩니다. 그래서 뷰를 찾고 뷰가 렌더링됩니다.

 그런대 RestController 는 HTTP message body에 바로 입력됩니다. 

 따라서 "ok" 를 리턴하면 실행 결과로 ok 를 바로 받을 수 있습니다.

 

2) Logger

: sl4j 라이브러리에 해당하는 로거를 사용했습니다.

 

3) log.info 명령문

- trace

- debug

- info

- warn

- error 

로거는 다섯가지 메서드를 가지고 있습니다. 그냥 실행시키면 default 가 info 이기 때문에  trace 와 debug 는 출력하지 않습니다.

 

하지만 application.properties 에

logging.level.package명=trace

로 설정하면 그 하위인

trace, debug, info, warn, error 가 다 나옵니다.

그리고 debug 로 설정하면 debug, info, warn, error  가 나옵니다.

 

그래서 주로 운영서버에서는 info 로 설정하고, 개발서버는 debug, 로컬 환경에서는 trace 로 모든 로그를 확인합니다.

 

만약 패키지별로 로그 레벨을 설정하는게 아닌 프로젝트 루트에 대한 로그를 설정한다면

logging.level.root=trace

로 설정하면 됩니다.

 

 

다음과 같은 출력 로그를 간단히 확인할 수 있습니다.

 

 

@Sl4j

위의 로깅 변수를 선언할 필요 없이 다음 애노테이션을 넣어주면 위의 로거를 바로 사용할 수 있습니다.

 

'스터디' 카테고리의 다른 글

로그인 페이지는 GET ? POST ? (2)  (0) 2022.12.04
로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01
Spring MVC  (0) 2022.11.30
MVC 패턴  (0) 2022.11.28
백엔드에서의 HTML, HTTP 간단한 요점  (0) 2022.11.24

DispacherServlet

HttpServlet 을 상속 받으며, 서블릿으로 동작합니다.

그래서 스프링 부트는 DispacherServlet 을 서블릿으로 자동 등록하면서 모든 경로에 대해 매핑합니다.

 

 

요청 흐름

1) 서블릿이 호출되면 HttpServlet 이 제공하는 service() 가 호출됨

2) Spring MVC에서 DispatcherServlet 의 부모인 클래스의 service() 를 호출하면서

     DispacherServlet.doDispatch() 가 호출됩니다.

 

doDispatch()

1) 핸들러 조회 : mh = getHandler()

    - 핸들러 매핑을 통해 URL 에 매핑된 핸들러를 조회

 

2) 핸들러 어댑터 조회 : ha = getHandlerAdapter()

    - 핸들러를 실행할 수 있는 핸들러 어댑터 조회

 

3) 핸들러 어댑터 실행 : ha.handle(request, response, mh.geHandler() )

     => ModelAndView 반환

    - 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView 로 변환해서 반환

 

4) 뷰 리졸버를 통해 뷰 찾기 => 뷰 반환

    - 뷰 리졸버를 찾아 실행

뷰 리졸버의 역할 :
뷰의 논리를 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.

5) 뷰 렌더링

    - 뷰를 통해 뷰를 렌더링

 

 

DispacherServlet 의 장점

코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다는 점입니다.

그 이유는 대부분의 핵심 기능이 인터페이스로 제공되기 때문입니다.

 

 

주요 인터페이스

1) 핸들러 매핑 : HandlerMapping

2) 핸들러 어댑터 : HandlerAdapter

3) 뷰 리졸버 : ViewResolver

4) 뷰 : View

 

 

'스터디' 카테고리의 다른 글

로그인 페이지는 GET ? POST ? (1)  (0) 2022.12.01
logging 알아보기  (0) 2022.11.30
MVC 패턴  (0) 2022.11.28
백엔드에서의 HTML, HTTP 간단한 요점  (0) 2022.11.24
멀티 쓰레드  (0) 2022.11.24

MVC 패턴을 왜 사용할까?

서블릿, JSP 등 다양한 방식으로 동적인 웹페이지를 구성할 수 있습니다.

그런데 왜 굳이 MVC 패턴을 사용할까요?

그건 유지보수성기능 특화 때문이라고 생각됩니다.

 

라이프 사이클의 변경

예를 들어, UI를 일부 수정하는 일과 비지니스 로직을 수정하는 일은 다른 일입니다.

그런데 이렇게 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수에 좋지 않습니다.

 

기능 특화

JSP 와 같은 뷰 템플릿은 화면 렌더링에 최적화되어 있기 때문에 이 부분의 업무만 담당하는것이 효과적입니다.

 

 

 

MVC 란?

Model

뷰에 출력할 데이터를 담아둡니다.

이 역할 덕분에 뷰는 비지니스 로직이나 데이터 접근을 몰라도 되고,  화면에 렌더링하는 일에만 집중할 수 있게 됩니다.

 

View

모델에 담겨져 있는 데이터를 화면에 그려줍니다. HTML 을 생성하는 부분입니다.

 

Controller

HTTP 요청을 받아서 피라미터를 검증하고, 비지니스 로직을 실행해줍니다.

그리고 뷰에 전달할 데이터를 조회해서 모델에 담아줍니다.

 

 

* 참고

컨트롤러에서 비지니스 로직을 두면 너무 많은 역할을 하게 됩니다.

그래서 서비스 계층을 따로 만들어서 비지니스 로직을 처리합니다. 컨트롤러는 이 서비스를 호출하는 담당을 하게 됩니다.

 

 

 

MVC 패턴의 문제점

  • 포워드 중복

RequestDispatcher 을 통해 리퀘스트의 뷰를 가져오고, 포워드를 통해 jsp 에 전달하게 되는 과정을 말합니다.

즉, 뷰로 이동하는 코드가 중복되어 호출됩니다.

 

이 외에도 뷰 패쓰도 중복되고, 사용하지 않는 코드들이 들어가게 됩니다.

 

그래서 기능이 복잡해질 수록 컨트롤러에서 공통으로 처리해야할 부분이 증가되는데 그 공통 메서드를 계속 호출하게 됩니다.

 

 

해결 방법

이러한 문제를 해결하는 방법이 프론트 컨트롤러 패턴을 도입하는 것입니다.

서블릿이 호출되기 전에 공통 기능들을 다 처리하는 방법입니다.

 

 

'스터디' 카테고리의 다른 글

logging 알아보기  (0) 2022.11.30
Spring MVC  (0) 2022.11.30
백엔드에서의 HTML, HTTP 간단한 요점  (0) 2022.11.24
멀티 쓰레드  (0) 2022.11.24
서블릿  (1) 2022.11.22

이번에 해볼 상황은 다음과 같습니다.

 

HTML Form 을 통해 회원 데이터를 가입하면, 새로운 페이지에서 그 데이터를 보여주는 것입니다.

 

 

Form Servlet 구현

package servletTest.servlet.webServlet;

import servletTest.servlet.repository.MemberRepository;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class memberFormServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");

        PrintWriter w = response.getWriter();
        w.write("<!DOCTYPE html>\n" +
                "<html>\n" +
                "<head>\n" +
                "    <meta charset=\"UTF-8\">\n" +
                "    <title>Title</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<form action=\"/servlet/members/save\" method=\"post\">\n" +
                "    username: <input type=\"text\" name=\"username\" />\n" +
                "    age:      <input type=\"text\" name=\"age\" />\n" +
                " <button type=\"submit\">전송</button>\n" + "</form>\n" +
                "</body>\n" +
                "</html>\n");

    }
}

url 주소 : /servlet/members/new-form

해당 주소에 접속하게 되면 response 에 html 형태로 응답을 보내게 됩니다.

 

form action 에 데이터를 보낼 ur 을 작성해줍니다.

 

Form 데이터 받아서 출력하기

package servletTest.servlet.webServlet;

import servletTest.servlet.domain.member.Member;
import servletTest.servlet.repository.MemberRepository;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class memberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void  service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MemberSaveServlet.service");

        // 요청 데이터
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        // 저장
        Member member = new Member(username, age);
        memberRepository.save(member);

        // 응답 메시지
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" + "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                "    <li>id="+member.getId()+"</li>\n" +
                "    <li>username="+member.getUsername()+"</li>\n" +
                " <li>age="+member.getAge()+"</li>\n" + "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
                "</html>");
    }
}

받아온 데이터를 Member 객체에 저장해주고, 싱글톤으로 구현한 Repository 를 통해 저장해줍니다.

 

그리고 받아온 데이터를 응답메시지로 다시 HTML 형태로 출력해줍니다.

데이터를 전송을 하면

성공적으로 데이터가 출력이 됨을 볼 수 있습니다.

 

 

마무리 정리하면...

Resource 아래에 Html 파일을 작성하여 Form 을 구성할 수 있습니다.

하지만 왜 굳이 서블릿을 통해 불편하게 HTML 파일을 작성할까요?

 

그 이유는 동적으로 HTML 파일을 생성할 수 있기 때문입니다.

즉, 이미 만들어진 형태가 아닌 원하는 형태로 원할 때, 원하는 구성으로 HTML 파일을 구성할 수 있다는 장점입니다.

 

그래서 계속 달라지는 저장 결과나, 회원 목록 같은 동적인 HTML 만드는 일이 가능해집니다.

 

하지만 위의 코드를 보듯이 매우 비효율적인 코드 작성을 하게 됩니다.

 

그래서 HTML 파일 안에 변경해야하는 부분만 자바 코드를 넣을 수 있도록 한 것이 템플릿 엔진입니다.

 

템플릿 엔진에는 [ JSP , Thymeleaf, Freemarker 등 ] 이 있습니다.

 

하지만 JSP 는 현재 점점 없어지는 추세이기 때문에 Spring MVC 패턴을 가지고 토이 프로젝트를 진행하도록 하겠습니다.

 

HTTP 응답 메시지 생성

  • HTTP 응답코드 지정
  • 헤더 생성
  • 바디 생성

* 편의기능 : Content-Type 편리하게 지정, 쿠키를 객체로 관리할 수 있는 방법, Redirect 기능

 

 

 

헤더 생성하기

[ 소스코드 ]

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // [status-line]
        response.setStatus(HttpServletResponse.SC_OK); // HTTP 응답 코드 넣기

        //[response-header]
        response.setHeader("Content-Type", "text/plain");
        response.setHeader("Cache-Control", "no-cache, no-store, must- revalidate"); // 캐시 무효화
        response.setHeader("Pragma", "no-cache");
        response.setHeader("my-header","hello"); // 임의의 헤더 만들기

        PrintWriter writer = response.getWriter();
        writer.println("ok");
 }

 

[ 테스트 확인 ]

url : http://localhost:8080/response-header

Response Header 가 정상적으로 생성됬음을 알 수 있습니다.

 

 

Header 편리하게 생성하기

[ 소스코드 ]

private void content(HttpServletResponse response) {
        //Content-Type: text/plain;charset=utf-8
        // Content-Length: 2
        //response.setHeader("Content-Type", "text/plain;charset=utf-8");
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        //response.setContentLength(2); //(생략시 자동 생성)
}

위의 코드를 작성하면 직접 타이핑할 필요 없이 content 에 response 만 넣어주면 자동으로 헤더가 생성됩니다.

 

 

 

Cookie 편리하게 생성하기

[ 소스코드 ]

private void cookie(HttpServletResponse response) {
        //Set-Cookie: myCookie=good; Max-Age=600;
        // response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
        Cookie cookie = new Cookie("myCookie", "good");
        cookie.setMaxAge(600); //600초
        response.addCookie(cookie);
}

쿠키가 정상적으로 출력됨을 볼 수 있습니다.

 

 

Redirect 사용하기

1) 상태코드 : 302

= setStatus( HttpServletResponse.SC_FOUND )

 

2) 로케이션 : /basic/hello-form.html 로 보냄

= setHeader ( "Location", 디렉토리 )

 

[ 소스코드 ]

private void redirect(HttpServletResponse response) throws IOException {
        //Status Code 302
        //Location: /basic/hello-form.html
        //response.setStatus(HttpServletResponse.SC_FOUND); //302
        //response.setHeader("Location", "/basic/hello-form.html");
        response.sendRedirect("/basic/hello-form.html"); // 4,5 번째 줄은 이 한줄로 해결가능
}

 

정상적으로 리다이렉트 됩니다.

 

 

JSON 데이터를 java 객체에 담아 출력하기

private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        helloData helloData = new helloData();
        helloData.setUsername("jun");
        helloData.setAge(30);

        String result = objectMapper.writeValueAsString(helloData);

        response.getWriter().write(result);
}

request 와 비슷하나 writeValueAsString ( helloData ) 라는 메서드를 사용한다는 차이점이 있습니다.

데이터가 정상 출력됩니다.

'HTTP' 카테고리의 다른 글

JSON 형식으로 받은 데이터를 객체로 변환하기  (0) 2022.11.26
HTTP 요청 데이터 작성하기  (0) 2022.11.26
DTO  (0) 2022.08.22
RestAPI 활용하기  (0) 2022.08.22
Message States Server  (0) 2022.08.19

JSON 형식 전송

  • POST http://localhost:8080/request-body-json
  • content-type = application/json
  • message body : {"username" : "hello", "age" : 20 }
  • 결과 : messagebody = {"username" : "hello", "age" : 20 }

 

 

JSON 형식 파싱 추가

JSON 형식으로 파싱할 수 있게 객체를 하나 생성

 

 

 

테스트 해보기

1) 소스코드는 Text 로 작성하는 것과 같습니다.

@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-body-json")
public class RequestBodyJsonServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("messageBody : " + messageBody);
    }
}

 

2) Postman 에서 JSON 데이터 사용하기

 

raw 에서 JSON 으로 변경만 해주면 됩니다.

3) 정상 작동 확인

 

 

 

JSON 데이터를 Java 객체로 변환하기

: JSON 을 변환시켜주는 라이브러리 필요

 

1) ObjectMapper object 객체 생성 ( SpringBoot 에서 기본적으로 지원해줌 )

2) objectMapper.readValue ( 메세지, Class ) 실행

3) 해당 메서드를 통해 자바 객체를 사용할 수 있게 됩니다.

 

[ 소스코드 ]

private ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("messageBody : " + messageBody);

        helloData helloData = objectMapper.readValue(messageBody, helloData.class);

        System.out.println("username : "+helloData.getUsername());
        System.out.println("age : "+helloData.getAge());

        response.getWriter().write("ok");
}

정상 출력됨이 확인됩니다.

'HTTP' 카테고리의 다른 글

HttpServletResponse 사용해보기  (0) 2022.11.26
HTTP 요청 데이터 작성하기  (0) 2022.11.26
DTO  (0) 2022.08.22
RestAPI 활용하기  (0) 2022.08.22
Message States Server  (0) 2022.08.19

HTTP 요청 메시지를 통한 데이터 전달 방법

 

GET
  • /url ? username = hello & age = 20
  • 메시지 바디 없이 URL 의 쿼리 피라미터에 데이터를 포함해서 전달
  • ex. 검색, 필터, 페이징 등에서 많이 사용하는 방식

[ 소스 코드 ]

 

package servletTest.servlet.request;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("전체 파라미터 조회 - start");
        request.getParameterNames().asIterator().forEachRemaining(paramName -> System.out.println(paramName + " : " + request.getParameter(paramName)));
        System.out.println("전체 파라미터 조회 - end");

        System.out.println("단일 파라미터 조회 - start");
        String username = request.getParameter("username");
        String age = request.getParameter("age");

        System.out.println("username = " + username);
        System.out.println("age = " + age);
        System.out.println();
        System.out.println("단일 파라미터 조회 - end");

        System.out.println("중복되는 복수 파라미터 조회");
        String[] usernames = request.getParameterValues("username");
        for (String i : usernames) {
            System.out.println("username = " + i);
        }

    }
}

* 여기서 주의할 점은 복수 파리미터에 대한 부분입니다.

http://localhost:8080/request-param?username=hello&age=20&username=hello2

와 같은 url 로 username 을 중복해서 부르게 된다면 그 값이 없어지는게 아니라,

username 안에 두 값을 받아오게 됩니다.

 

그래서 값들을 담는 배열 객체를 만들어 저장 후 출력이 가능합니다.

 

 

 

 

POST
  • content-type : application / x-www.form-urlencoded
  • 메시지 바디에 쿼리 피라미터 형식으로 전달 username = hello & age = 20 ( url 형태와 비슷함 )
  • ex. 회원 가입, 상품 주문, HTML form 사용

 

1) POST 를 사용하려면 HTML Form이 필요합니다.

- webapp/basic 경로에 hello-form.html 을 생성해줍니다.

- http://localhost:8080/basic/hello-form.html 로 브라우저 접속

 

[ 폼 형태 ]

<form action="/request-param" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" /> <button type="submit"> 전송 </button>
</form>

위의 폼을 보면 다음을 알 수 있습니다.

Form 전송을 누르게 되면 이전에 서블릿 경로로 만들어놨던 "/request-param" 경로로 전송하게 됩니다.

개발자도구를 켜서 폼 데이터를 보면 데이터가 제대로 들어왔음을 확인할 수 있습니다.

 

 

2) 위 데이터를 컨텐트 바디에 보내게 됩니다.

다음과 같이 엔코딩되어 전송 됩니다.

 

 

3) 데이터 확인

데이터가 정상적으로 출력되고 있음이 확인가능합니다.

 

 

 

 

[ 중간 정리 ]

1) request.getParameter 형식은 GET / POST 둘 다 지원한다.

2) POST 는 브라우저가 알아서 형식을 만들어서 전송한다.

3) GET 은 사용자가 Url 을 입력한다.

4) 즉, 클라이언트 입장에선 다르게 작성하지만

          서버입장에선 GET/POST 구분없이 조회가 가능합니다.

 

Q. 그럼 위와 같은 HTTP 메서드 테스트를 할 때, 매번 Html 파일을 만들어야 하는가?

A. 아니다. Postman 을 활용하자 !!

위의 과정을 Postman 을 통해 테스트한 결과, 200 OK가 나오면 정상 작동하는 것입니다.

 

 

HTTP message body 에 데이터 직접 담아서 요청
  • HTTP API 에서 주로 사용
  • JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용
  • POST, PUT, PATCH

[ 소스 코드 ]

@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();// 메세지 바디의 코드를 바이트 코드로 얻을 수 있음
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);// StreamUtils 를 통해 인코딩 정보를 알려주고 바이트 코드로 변환 시켜줌

        System.out.println("messageBody : " + messageBody);
        response.getWriter().write("ok");
    }
}

[ Postman Test ]

정상적으로 출력됨을 볼 수 있습니다.

'HTTP' 카테고리의 다른 글

HttpServletResponse 사용해보기  (0) 2022.11.26
JSON 형식으로 받은 데이터를 객체로 변환하기  (0) 2022.11.26
DTO  (0) 2022.08.22
RestAPI 활용하기  (0) 2022.08.22
Message States Server  (0) 2022.08.19

HttpServletRequest 역할

- HTTP 를 직접 파싱하는 경우

: 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 할 일이 많아집니다.

 

- 서블릿 필요 이유

서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자를 대신해서 HTTP 요청 메시지를 파싱합니다.

 

- HttpServletRequest 의 존재 이유

그 요청 메시지에 대한 결과를 HttpServletRequest 객체에 담아서 제공합니다.

 

 

 

HttpServletRequest 예시

- HTTP 요청 메시지

POST /save HTTP/1.1
Host : localhost: 8080
Content-Type : application/x-www-form-urlencoded

username=park&age=20

이와 같은 요청 메시지를 직접 매번 파싱한다는 의미입니다. 

 

각 문장에 대해 HttpServletRequest 가 하는 역할은

- 첫 번째 줄 : START LINE [ HTTP 메소드, URL, 스키마 ]

- 두, 세 번째 줄 : 헤더 [ 헤더 조회 ]

- 바디 : 한 줄 띄우고 나머지 부분 [ form 피라미터 형식 조회, 메세지 바디 데이터 직접 조회 ]

 

 

 HttpServletRequest 객체의 여러가지 기능

- 임시 저장소 기능

HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능

request.setAttribute ( name, value )

request.getAttribute ( name )

 

- 세션 관리 기능

request.getSession ( create: true )

 

 

HTTP Request 해보기

PostMan 설치 후 HTTP 메서드를 테스트 해봤습니다.

위와 같이 Request 메서드에 대해 print 할 수 있는 메서드를 작성하였습니다.

 

그 후 POST 로 메세지를 보내면

위와 같이 요청 메시지가 정상적으로 들어옴을 확인할 수 있습니다.

+ Recent posts