[Spring]

kodaaa·2022년 7월 26일
0

Spring

목록 보기
1/1

어노테이션은 클래스 선언 바로 앞/필드 선언 바로 앞에 붙여준다.

@ToString

toString() 메소드를 자동으로 생성해준다.
exclude : 특정 필드를 toString() 결과에서 제외시킨다.

@ToString(exclude = "pw")
public class User {
  private Long id;
  private String name;
  private String pw;
  private int age;
}
User user = new User();
user.setId(1L);
user.setName("user");
user.setPw("1234");
user.setAge(20);

System.out.println(user);
// User(id=1, name=user, age=20)

@Builder

📖 참고
생성자에서 인자가 많고, 필수로 값을 받아야 할 필드와 선택적으로 받아도 되는 필드가 섞여있을 때 사용한다.
코드량이 줄어들고, 객체의 일관성을 유지할 수 있다.

  • 객체의 일관성이 깨진다는 것은, 한번 객체를 생성할때 그 객체가 변할 여지가 있다는 것
@Builder
public class Person {
  private final String name;
  private final int age;
  private final int phone;
}

다음과 같이 객체 생성 가능!

Person person = Person.builder() // 빌더어노테이션으로 생성된 빌더클래스 생성자
    .name("seungjin")
    .age(25)
    .phone(1234)
    .build();

@RequiredArgsConstructor

final인 필드만 가지고 생성자를 자동으로 만들어줌

@AllArgsConstructor

모든 필드를 가지고 생성자를 자동으로 만들어줌

final : 반드시 최초에 초기화 해야 함, 그 이후 수정 불가

@Component

Component 클래스 앞에 붙임
Bean Configuration 파일에 Bean을 따로 등록하지 않아도 사용할 수 있다.

자바 hashmap
https://coding-factory.tistory.com/556
HashMap은 내부에 '키'와 '값'을 저장하는 자료 구조를 가지고 있습니다. 사용자는 그 위치를 알 수 없고, 삽입되는 순서와 들어 있는 위치 또한 관계가 없습니다.

엔티티 클래스에 파라미터가 있는 생성자를 별도로 만들었으면, 기본생성자(파라미터가 하나도 없는 생성자 - @NoArgsConstructor로 만듦!)도 직접 만들어줘야 함 - JPA, Mybatis 모두 ㅇㅇ
@AllArgsConstructor(access = AccessLevel.PRIVATE) 이거 아마 이 생성자 쓸모없는데 있어야 해서 저렇게 굳이 접근제한 PRIVATE로 두는건가?

mybatis는 entity(vo)에 pk, fk이런거 명시를 못하네ㅠㅠ 그냥 필드명만 들어가서 불편함

코드가 필드인 경우에는 fk취급 안하는듯

컨트롤러단 매개변수, return문에서는 무조건 DTO사용(엔티티/VO 그대로 사용x)

엔티티-DTO변환을 어떤 layer에서 하는게 좋을까?

  • 구글링해보면 controller vs service 논란의 여지가 많음
  • 김영한님은, 'Service에서도 Entity를 반환하고, 그 후 Controller에서 Entity -> Dto 변환 작업을 하면 된다'고 하심
    레포지토리에서는 VO, DTO 모두 사용 가능한듯

@JsonIgnore

Response에 해당 필드가 제외된다
(엔티티의)필드앞에 붙임
보통 DTO 대신 엔티티에 Response에 담지 않을 필드에 붙여서 사용하는듯
근데 하나의 엔티티가 여러군데에서 쓰이고, 어떨때는 이 필드가 사용될때도, 안사용될때도 있으니 복잡해지므로, DTO를 따로 파는 것이 좋고, JsonIgnore은 지양됨.

근데 dto에도 쓰네..? 뭐지

Spring Data Repository Interface에서 CRUD 기본 메소드 제공
https://escapefromcoding.tistory.com/377
find, update, delete, existsById등..
이 외에 직접 커스텀한 메소드들을 선언하려면 resources>mapper>레포지토리클래스 이름.xml에 따로 쿼리문을 정의해두면 됨 ex.PointDao.xml

@Data

아래 어노테이션들을 포함

  • @ToString
  • @EqualsAndHashCode
  • @Getter : 모든 필드
  • @Setter : 정적 필드가 아닌 모든 필드
  • @RequiredArgsConstructor

dao에 정의한 메소드를 mapper 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">

<!--이 메소드들이 선언된 DAO 위치-->
<mapper namespace="com.example.CinemaSeoulCopy.dao.user.PointDao">

    <!--[포인트 입력]-->
    <insert id="updatePoint" parameterType="PointVo">
        INSERT INTO POINT (POIN_ID, USER_ID, POIN_AMOUNT, POIN_TYPE_CODE, MESSAGE)
        VALUES (SEQ_POINT.NEXTVAL, #{user_id}, #{poin_amount}, #{poin_type_code}, #{message})
    </insert>
</mapper>
  • <insert id="updatePoint" parameterType="PointVo">
            INSERT INTO POINT (POIN_ID, USER_ID, POIN_AMOUNT, POIN_TYPE_CODE, MESSAGE)
            VALUES (SEQ_POINT.NEXTVAL, #{user_id}, #{poin_amount}, #{poin_type_code}, #{message})
    </insert>
    • <insert> : CRUD중 C
      • insert(C), select(R), update(U), delete(D)
    • id : DAO에 선언한 메소드 이름
    • parameterType : DAO에 선언한 메소드의 파라미터 타입
      • insert, update, delete, select 시에 생략 가능. 대부분 생략함.
    • #{파라미터의 필드명} = 해당 필드명의 값을 불러옴
  •   <!--[포인트 조회]-->
      <select id="getPoint" parameterType="Integer" resultType="UsersVo">
          SELECT USER_ID, USER_NAME, CURR_POINT, ACCU_POINT
          FROM USERS
          WHERE U.USER_ID = {user_id}
      </select>
    • resultType : DAO에 선언한 메소드의 return 타입
      • select에서는 생략 불가
    • parameterType, resultType 모두 DTO, VO, 다른 타입 등 어떤 타입도 가능!

현재 포인트 실제 ( ) 존재 와 누적 포인트 사용을 ( 포함하여 누적된 모든 포인트 로)

CDATA

<![CDATA[ ]]> 이렇게 선언하고 안에 문자열을 채워 넣으면 , [ ] 안에 있는 문장은 파싱되지 않고 그대로 문자열로 출력된다.
마이바티스에서 매퍼 파일은 XML으로 작성되어 있고, 파싱될 때 XML 표준으로 파싱된다.
SELECT문에는 조건을 걸어 쿼리하기 위해 <, >, = 등의 기호를 많이 사용하는데, 이것이 파싱 중에 태그로 인식되거나 하는 등의 문제가 생길 수 있다.
<![CDATA[ ]]> 안에 원하는 쿼리문을 선언 한다면, 파싱하지 않고 그대로 문자열로 인식 시킬 수 있어 이런 문제를 예방할 수 있다.
동적 SQL에서는 사용하지 못하는데, 필요한 특수문자에 한해서만 적용시키면 동적 SQL에서도 사용 가능하다.

TO_CHAR

날짜 등을 문자열로 변환하는 함수
https://gent.tistory.com/331

SELECT TO_CHAR(SYSDATE, 'YYYYMMDD')              --20200723
     , TO_CHAR(SYSDATE, 'YYYY/MM/DD')            --2020/07/23
     , TO_CHAR(SYSDATE, 'YYYY-MM-DD')            --2020-07-23
     , TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') --2020-07-23 11:10:52
  FROM dual

if문

https://java119.tistory.com/42

<if test="조건문">
조건문이 참이면 실행할 쿼리문 일부
</if>

조건문은 테이블의 필드명이 아닌 파라미터의 필드가 들어감
형태도 그냥 java 형태

<select id="findPoint" resultType="PointInfoDto">
        SELECT P.POIN_ID, P.POIN_AMOUNT, (SELECT CODE_NAME FROM CODE C WHERE C.CODE_ID = P.POIN_TYPE_CODE) AS POIN_TYPE,
            P.MESSAGE, P.POIN_DATETIME
        FROM POINT P
        WHERE P.USER_ID = #{user_id}
            <if test="start_date != null"><![CDATA[
            AND #{start_date} <= TO_CHAR(POIN_DATETIME, 'YYYYMMDD')
            ]]></if>
    </select>

dao.xml에서 왜 어떤건 parameterType 적고 어떤건 안적지?
근데 인터넷에서는 파라미터 여러개 쓰려면 Vo로 엮거나 맵 쓰라는데..?
음.. 그냥 parameterType을 안써버리면 문제 없는건가? 대신 타입검사는 안해줄것같기도 하고
대신 쓸거면 dao.java파일의 메소드의 파라미터 타입과 parameterType을 완전히 맞춰야 함
맵(해쉬맵) 쓰는 방법 : https://codevang.tistory.com/276

selectKey

https://popo015.tistory.com/100
SQL 수행작업 중 insert된 이후에 알 수 있는 값 또는,
생성된 값을 바로 가져와서 select 쿼리를 보내야 하는 경우가 있다.
주로 생성하고 난 후의 인덱스(번호)를 가져와 작업해야 하는 상황에서 많이 사용한다.
이런경우 java에서 insert 쿼리를 실행하고 값을 받은 후
값을 가지고 다시 쿼리를 DB에 전송하는 방법이 있다.
하지만, 불필요한 여러번의 DB 입출력은 시간이 느려진다는 단점이 있다.
그때, selectKey를 사용하여 바로 적용할 수 있다.
마이바티스, 아이바티스 두개 다 적용이 가능하다.
selectKey는 DB에 명령을 한번만 보내며, 우선 입력한 값의 결과값을 다음 쿼리로 바로 return 시켜주는 것이다.

Dual 테이블

https://goddaehee.tistory.com/92
사용자가 함수(계산)를 실행할 때 임시로 사용하는데 적합하다.

SELECT SEQ_USERS.nextval FROM DUAL

ex) SELECT 시퀀스.NEXTVAL FROM DUAL;
ex) SELECT SYSDATE FROM DUAL;
ex) SELECT CURRENT_DATE FROM DUAL;
ex) Merge into 내부에서 사용

시퀀스

https://mine-it-record.tistory.com/61

  • 유일한 값을 생성해주는 오라클 객체이다.
  • 일련번호, 자동증가 값을 생성한다.
  • 시퀀스는 독립적으로 테이블과 별개로 동작한다.
  • 시퀀스 이름: SEQ_테이블이름

NEXTVAL, CURRVAL
https://mine-it-record.tistory.com/62
testSeq가 시퀀스라고 할 때
-- 해당 시퀀스의 값을 증가시키고 싶다면
testSeq.NEXTVAL
-- 현재 시퀀스를 알고 싶다면
testSeq.CURRVAL

AS 생략

(SELECT CODE_NAME FROM CODE C WHERE C.CODE_ID = USERS.USER_TYPE_CODE) USER_TYPE

USER_TYPE 앞에 AS 생략되어있음. 생략 가능!
CODE_ID가 곧 코드

@Transactional

https://pjh3749.tistory.com/269
클래스 또는 메소드 정의 앞에 붙임

  • 특정 메소드에만 데이터변경이 있을 경우, 메소드 정의 앞에 붙이기
  • 데이터 변경, 로직은 트랜잭션 안에서 실행되어야 함. 이 어노테이션을 붙이면 트랜잭션 안에서 실행됨
  • @Transactional(readOnly = true) : 데이터 변경 없이 조회만 하는 기능이 많을 경우에 클래스 이름 앞에 붙이기 -> 성능 up
  • @Transactional(rollbackFor = {이 예외가 발생했을때 롤백해야하는 예외.class}
    • ex. @Transactional(rollbackFor = {CheckedException.class}
    • 모든 예외에 대해 전부 트랜잭션을 롤백하고 싶다면
      @Transactional(rollbackFor = {Exception.class}
    • 스프링은 RuntimeException 과 Error를 기본적으로 롤백 정책으로 이해한다.
      @Transactional(rollbackFor = {RuntimeException.class, Error.class}

insert, update, delete문(SQL)의 return값

insert, update, delete에는 resultType이 없고
row의 개수를 반환한다.
insert의 경우는 삽입된 행의 개수를 반환
update의 경우는 수정에 성공한 행의 개수를 반환(실패시 0 반환)
delete의 경우는 삭제한 행의 개수를 반환

Exception 처리

  • 따로 exception 패키지를 파서 ExceptionController, custom exception 클래스들을 정의하기

  • ExceptionController

    • Controller 단에서 발생하는 예외들을 어떻게 처리할지 정의

    • 원래는 예외가 발생할 때마다 try catch 문으로 일일이 예외 처리를 해줘야 하는데, 여기에 정의된 예외는 controller에서 발생하면 자동으로 처리해줌

    • servcie 단에서 발생한 예외도 controller의 함수를 호출하기만 하면 exceptionController가 처리해줌

    • @ControllerAdvice
      @Slf4j
      public class ExceptionController {
      
          //401
          //DuplicateException이 발생하면 메소드에 정의된 대로 처리 : 401 Unauthorized 에러를 발생시킴
          @ExceptionHandler({DuplicateException.class})
          public ResponseEntity<?> UnAuthorizedException(final DuplicateException ex){
              return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage());
          }
      }
  • custom exception

    • //이 예외가 발생하면 BAD_REQUEST 응답을 반환
      @ResponseStatus(HttpStatus.BAD_REQUEST)
      public class WrongArgException extends RuntimeException{
      
          //private static final String MESSAGE = "메세지"; //발생할 메세지 일률적으로 처리 가능
          
          //이 응답을 발생시킬 생성자
          //throw new WrongArgException("에러메세지"); 형식으로 에러 발생시킴
          public WrongArgException(String message) {
              super(message);
          }
      }
    • @ResponseStatus(HttpStatus.BAD_REQUEST) : 응답의 상태 코드를 지정

      • 아무것도 지정하지 않으면 200 OK가 반환된다.
      • 즉 controller 어딘가에서 저 예외가 발생하는 경우 BAD_REQUEST 응답을 반환!
  • 예외는, 예외가 발생할 수 있는 메소드 정의시에 thorws Exception 붙이고, throw new WrongArgException("message..."); 이런 식으로 발생시킴

  • 참고할 블로그 : https://bloowhale.tistory.com/72 , https://kdg-is.tistory.com/221?category=983893

생성메소드(createOrder), 비즈니스로직(주문취소 등), 조회로직(전체주문가격조회) 은 해당 엔티티(VO) 클래스에 넣자

계층 정리

  • VO(Entity)
    • DB 테이블의 필드 구성 = VO의 필드 구성
    • 보통 VO를 파라미터로 해서 INSERT 쿼리문을 통해 테이블에 넣음
  • DAO(Repository)
    • 테이블에서 데이터를 insert, select, update, delete함
    • interface로 메소드 선언만 해두고, 쿼리문은 ㅇㅇDao.xml에 있음
  • Service
    • DAO를 final로 선언해서 사용
    • 회원가입, 회원조회 등 기능별로 메소드 생성
    • DAO의 메소드를 사용해서 이것저것 수행함
    • DTO보다는 VO를 다룸(DAO의 메소드의 파라미터, return으로 VO가 사용됨)
      • DTO도 다루긴 함!!! 때에 따라 다름
    • 데이터 변경이 있는 기능(메소드)의 경우 트랜잭션 안에서 실행 (@Transaction)
    • (VO-> DTO변환이 일어나는 지점)
  • Controller
    • post, get, delete
    • URL별로 메소드 지정하여, 클라이언트에서 그 url로 요청을 보냈을 때 실행할 것들을 정의
    • 메소드의 파라미터, return값은 절대 VO면 안됨. 무조건 DTO!
    • VO-> DTO변환이 일어나는 지점(영한띠 추천)

resultMap

https://velog.io/@vgo_dongv/Spring-%EC%9D%BC%EB%8C%80%EB%8B%A4-%EA%B4%80%EA%B3%84
계층형 데이터구조

trim

https://velog.io/@joyfuljoyful/MyBatis-%EB%8F%99%EC%A0%81%EC%BF%BC%EB%A6%AC-trimwhereset
필터링/키워드 검색기능 구현시 많이 사용
(조건에 따라 쿼리문을 다르게 하는 경우)

  • prefix
    • <trim>문에 의해 생성되는 SQL 구문 앞에 추가적인 구문을 넣어줍니다.
  • prefixOverrides
    • <trim>문에 의해 생성되는 SQL 구문 앞에 해당 문자가 있으면 자동으로 지워줍니다.

SELECT * FROM board WHERE

choose

https://velog.io/@joyfuljoyful/MyBatis-if
if와 달리 choose는 여러 상황들 중 하나의 상황에서만 동작합니다.
Java의 'if~else'와 유사하다.
<otherwise>는 위의 모든 조건이 만족되지 않을 경우에 사용합니다.

<choose>
  <when test="type == 'T'.toString()">
  	(title like '%'||#{keyword}||'%')
  </when>
  <when test="type == 'C'.toString()">
  	(content like '%'||#{keyword}||'%')
  </when>
  <when test="type == 'W'.toString()">
  	(writer like '%'||#{keyword}||'%')
  </when>
  <otherwise>
  	(title like '%'||#{keyword}||'%' OR content like '%'||#{keyword}||'#')
  </otherwise>
</choose>

IN (SQL)

WHERE 절 내에서 특정값 여러개를 선택하는 SQL 연산자
괄호 내의 값 중 일치하는 것이 있으면 TRUE
SELECT * FROM 테이블명
WHERE 컬럼명 IN (값1, 값2, ...);

foreach(MyBatis)

https://java119.tistory.com/85
collection : 전달받은 인자. List or Array 형태만 가능
item : 전달받은 인자 값을 alias 명으로 대체
open : 구문이 시작될때 삽입할 문자열
close : 구문이 종료될때 삽입할 문자열
separator : 반복 되는 사이에 출력할 문자열
index : 반복되는 구문 번호이다. 0부터 순차적으로 증가

내가 생각하는 MYBATIS 장점, 단점

  • 장점
    • MYSQL 쿼리문을 그대로 쓸 수 있으므로 복잡한 쿼리문을 쓸 때 편함
      • 영화관 서비스는 테이블 수가 적지 않았고(대형 서비스에 비하면 굉장히 적다고 하면 어떡하지) 서로 연관관계가 많아서 쿼리문을 그대로 쓰는 것은 엄청난 장점

어떻게 구현했는지 포인트들

  • 영화 검색 필터
    • 검색필터의 기준은 개봉상태, 이용연령제한, 장르
      • 모두 여러개 선택 가능(ex.여러가지 장르)
    • 3가지 기준 중에 검색필터 input으로 들어오는게 없을 수도 있을수도 있음, 선택 개수도 모두 다를 수 있음. 즉 조건에 따라 쿼리문이 바뀔 여지가 많음.
    • mybatis의 trim, choose, when문을 사용해 조건에 따라 구문을 추가하거나 제거함으로써 해결
    • +) 출연진타입(배우/감독) + 출연진 이름으로 영화 검색 기능 - 해당 출연진이 있는 영화 보여줌
  • 코드 관리
    • 코드값을 저장하고, 그 코드값을 pk로 하는 코드테이블에서 코드네임을 받아옴
      • 시청등급(all, 12, 15, 18), 개봉상태, 장르(액션, 멜로, ...), 유저타입(friends, family, vip), 포인트타입(적립, 사용, 취소), 유저권한(회원, 비회원), 관리자권한(매니저, 직원), 출연진타입(감독, 배우), 판매물품분류(스낵, 굿즈), 좌석타입(일반, 장애인용, 불가, 거리두기), 결제방법(신용카드, 간편결제, 핸드폰결제), 결제상태(완료, 취소, 사용완료)를 코드로 관리.
    • 코드의 분류(시청등급, 개봉상태, 장르...)를 상위코드로 하고, 분류값들을 하위코드로 하여 코드 구성
  • 메시지 관리
    • 메시지를 메시지코드로 관리
  • 장르(GENRE), 영화(MOVIE) 연결
    • MOVIEGENRE 테이블을 두어 영화의 장르정보를 관리
    • 이런걸 무슨 테이블이라고 하더라..?
  • 출연진(PEOPLE), 영화(MOVIE) 연결
    • CAST 테이블을 두어 영화의 출연진정보를 관리
  • 기본키 고려 순서
    • REVIEW 테이블 : MOVI_ID -> USER_ID
      • 영화에 대한 관람평을 검색하는 경우가, 그 반대의 경우보다 많다고 판단하여 MOVI_ID 우선
      • 하지만 마이페이지에서 자신이 쓴 리뷰를 모아보는 기능이 있어, USER_ID에 대한 인덱스 역시 추가 생성
    • SEAT 테이블 : HALL_ID -> SEAT_NUM
      • 상영관에 속한 좌석을 검색하는 경우가 훨씬 많다고 판단
    • AUDIENCERECORD : RECO_DATE -> MOVI_ID
      • 해당 기간에 해당하는 관람객 수를 정기적으로(매일?) 계산하므로 RECO_DATE 우선
      • 하지만 영화의 누적관람객을 계산할 때 MOVI_ID를 통해서도 접근하기 때문에 이에 대한 인덱스 역시 추가 생성
    • USERTYPERECORD : USER_ID -> UPDA_DATETIME
      • 특정 이용자를 기준으로 조회하는 일이 훨씬 빈번(ex.마이페이지)
    • MOVIEGENRE : MOVI_ID -> GENRE_CODE
      • 영화에 대한 장르를 검색하는 일이 빈번
      • 하지만 장르별 영화조회 기능이 있기 때문에, GENRE_CODE에 대한 인덱스 역시 추가 생성
    • BOOKSEAT : SHOW_ID -> SEAT_NUM
      • 상영일자에 대한 예매된 좌석을 검색하는 경우가 훨씬 빈번

ROWNUM

https://jhnyang.tistory.com/454

  • 특정 개수만큼 원하는 데이터를 추출하고 싶을 때 (ex. 연봉이 가장 높은 10명을 추출하고 싶을 때)
  • 데이터에 순번을 매기고 싶을 때

LIKE

https://gent.tistory.com/401
부분을 포함하고 있는 데이터를 검색할 때

SUBSTR

문자열 자르기
인덱스는 0부터 시작이 아닌 1부터 시작

  • SUBSTR(문자열,시작위치)
    • SELECT SUBSTR('안녕하세요ABC', 3) FROM DUAL;
    • 결과 : 하세요ABC
  • SUBSTR(문자열,시작위치,길이)
    • SELECT SUBSTR('안녕하세요ABC', 1, 2) FROM DUAL;
    • 결과 : 안녕
    • 길이가 2입니다. 첫번쨰 문자열인 '안'부터 두번째 문구인 '녕'까지 출력이 되었습니다.
  • SUBSTR(문자열,시작위치)
    • SELECT SUBSTR('안녕하세요ABC', -4) FROM DUAL;
    • 결과 : 요ABC
    • 시작위치에 음수를 입력하게되면 뒤에서부터 문자열을 자르게 됩니다.
    • 뒤에서 4번째 위치인 '요'부터 출력이 되었습니다.
  • SUBSTR(문자열,시작위치,길이)
    • SELECT SUBSTR('안녕하세요ABC', -4, 2) FROM DUAL;
    • 결과 : 요A
    • 시작위치는 -4로 뒤에서 네번쨰인 '요'부터 길이가 2자리인 문자열을 출력합니다.

jdbcType

MyBatis로 개발 시 입출력 변수의 javaType이나 jdbcType을 명시할 때 사용
ex. INTEGER(int), VARCHAR(String)
타입 표 : https://itmoon82.tistory.com/23

mapStruct

MapStruct은 엔티티와 DTO간의 매핑을 손쉽게 하게끔 도와준다.
-컴파일 시점에 맵핑클래스를 생성함(그래서 매우 좋음 바로바로 확인가능)

https://cchoimin.tistory.com/entry/model-mapping-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Mapstruct

@NotNull, @NonNull, @Nullable

@Nullable, @NotNull
@NotNull을 변수에 붙여서 검증에 사용할 수 있다. 하지만 메소드 인자에서 이 둘은 아무런 역할도 하지 않는다. 메소드 인자에서 이 둘은 단순한 문서 작성 도구로 작용한다.
유효성 검사시 메서드 인자에는 @NotNull이 아닌 @Valid 를 사용해야한다.
메소드 인자에서 유효성 검사하기 : lombok @NonNull
롬복에서 제공하는 @NonNull은 메서드의 인자에 사용하면 null이 들어올 시 NullPointerException을 발생시킨다.

profile
취뽀하자(●'◡'●)💕

0개의 댓글