2022.10.27 (목요일)

yeonicerely·2022년 10월 27일
0

1. 알고리즘: 해시(3)

프로그래머스 완주하지 못한 선수

Step 1

📝 참여자인 경우에 값을 초기화한 후 완주자인 경우 값을 변경
-> 초기화된 값을 유지하는 사람이 완주하지 못한 선수

작성한 코드

public class ARacerWithHashMap {

    public String solution(String[] participant, String[] completion){
        String whoDidNotFinish= null;

        Map<String, Integer> memo = new HashMap<>();

// -------- (1) ----------

        for (int i=0; i<participant.length; i++){
            String key = participant[i];
            memo.put(key, 1);
        }

        for (int i = 0; i < completion.length; i++) {
            String key = completion[i];
            memo.put(key, 0);
        }

// -------- (2) ----------

        for (String key:memo.keySet()) {
            if (memo.get(key) == 1){
                whoDidNotFinish = key;
            }

        }

        return whoDidNotFinish;
    }
}

테스트 코드

class ARacerWithHashMapTest {

    @Test
    void test(){
        ARacerWithHashMap aRacerWithHashMap = new ARacerWithHashMap();
        String personWhoDidNotFinish1 = aRacerWithHashMap.solution(new String[]{"leo", "kiki", "eden"}, new String[]{"eden", "kiki"});
        String personWhoDidNotFinish2 = aRacerWithHashMap.solution(new String[]{"marina", "josipa", "nikola", "vinko", "filipa"}, new String[]{"josipa", "filipa", "marina", "nikola"});
        String personWhoDidNotFinish3 = aRacerWithHashMap.solution(new String[]{"mislav", "stanko", "mislav", "ana"}, new String[]{"stanko", "ana", "mislav"});

        assertEquals(personWhoDidNotFinish1, "leo");
        assertEquals(personWhoDidNotFinish2, "vinko");
        assertEquals(personWhoDidNotFinish3, "mislav");
        // 동명이인 mislav가 동일하게 인식되어 null이 반환 -> 테스트 통과 X

    }

}

참가자에서 동명이인이 있을 때 (ex. ["mislav", "stanko", "mislav", "ana"]) 동명이인을 동일하게 인식하여 완주자가 ["stanko", "ana", "mislav"]일 때 "mislav"가 반환되는 것이 아니라 null이 반환됩니다.

Step 2: 동명이인 문제 해결

🔎 동명이인: 무조건 1 또는 0으로 초기화하는 것이 아니라 count를 올리거나 낮추어서 해결!

    public String solution(String[] participant, String[] completion){
        String whoDidNotFinish= null;

        Map<String, Integer> memo = new HashMap<>();

// -------- (1) ----------

        for (int i=0; i<participant.length; i++){
            String key = participant[i];

            // 동명이인 문제를 해결하기 위해 1로 초기화를 하는 것이 아니라 count를 올린다
            
            if(memo.get(key) != 1){// nullpointerException
                        
                memo.put(key, memo.get(key)+1);
            } else{
                memo.put(key, 1);
            }
        
// -------- (2) ----------

        for (String key:memo.keySet()) {
            if (memo.get(key) == 1){
                whoDidNotFinish = key;
            }
            
        }

        return whoDidNotFinish;
    }

getKey()함수에서 초기화 안된 key가 들어가는 경우 nullpoint exception 발생하게 됩니다.

Step 3: NullPointerException 문제 해결

🔎 NullPointerException: 초기화 된 상태인지 확인하는 메소드로 변경

// -------- (1) ----------
        for (int i=0; i<participant.length; i++){
            String key = participant[i];

            if(memo.containsKey(key)){
                memo.put(key, memo.get(key)+1);
            } else{
                memo.put(key, 1);
            }

containsKey() 메소드를 통해 초기화 되었는지를 확인합니다.

Step 4: 코드를 간단하게 리팩토링

getOrDefault() 메소드를 사용하여 코드를 더 간단하게 변경

// -------- (1) ----------
        for (String com:completion) {
            memo.put(com, memo.getOrDefault(com, 0)+1);
        }
        // getOrDefault(key, defaultValue)
        // : key가 존재하면 value를 return, 존재하지 않으면 defaultValue를 return
        
        
// -------- (2) ----------
        
        for (String com:completion) {
            memo.put(com, memo.get(com)-1);

        }

참가자를 확인 할 때 처음인 경우 1, 동명이인인 경우는 2,3,... 로 값을 설정해주기 때문에 getOrDefault에서 default 값을 0으로 설정해서 key가 Map에 존재하지 않으면 value로 0+1=1을 갖고 key가 존재하면 기존의 value에서 1 씩 더하도록 로직을 구현했습니다.

2. SpringBoot: API 작성하기

HTTP 메소드에는 GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE 총 8가지 종류가 있다. 이 중 GET, POST, PUT, DELETE를 사용하는 API에 대해서 살펴봅시다.

1) Get API 만들기

A. Get 메소드란?

  • 주로 데이터를 읽거나 검색할 때 사용하는 메소드입니다. 수정할 때는 사용하지 않습니다. 성공적으로 요청이 이루어지면 200 응답코드를 반환합니다.
  • Get 방식에서는 데이터를 요청할 때 데이터가 HTTP Request Message Header 부분의 url에 담겨서 전송됩니다. 따라서 url에 데이터가 그대로 담기기 때문에 데이터 크기가 제한적이고 보안에 취약하다는 특징이 있습니다.

B. @RequestMapping으로 Get 메소드 구현하기

@RestController
@RequestMapping("/api/v1/get-api")
public class GetController {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello(){
        return "Hello World";
    }

RequestMapping은 method를 지정하지 않으면 default로 모든 method(GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE)를 지원하므로 GET API를 구현하고 싶으면 RequestMethod.Get으로 지정해주어야합니다.

C. @GetMapping으로 Get 메소드 구현하기

스프링 4.3 이후 버전에서는 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 등과 같은 특정 HTTP 메소드에 맞는 annotation을 지원하기 때문에 이를 사용하여 API를 구현할 수 있습니다.

  • @GetMapping의 value를 설정하여 path를 지정할 수 있습니다.
@RestController
@RequestMapping("/api/v1/get-api")
public class GetController {

    @GetMapping(value = "/name") // path
    public String getName(){
        return "Yeonji";
    }

D. Path Variable(Path Parameter)을 이용해서 Get 메소드 구현하기

  • Path Variable: 주소를 통해서 값을 넘길 때 사용합니다

  • path에 들어가는 파라미터의 이름과 메소드에 들어가는 파라미터의 이름을 동일하게 설정해주어야 합니다.
    ex. {variable}과 @PathVariable String variable

@RestController
@RequestMapping("/api/v1/get-api")
public class GetController {

    @GetMapping(value = "/variable1/{variable}")
    public String getVariable1(@PathVariable String variable){
        return variable;
    }
  • 파라미터의 이름 variable을 메소드에 그대로 적기가 어려운 경우는 @PathVariable("파라미터이름")을 애용해서 Path Variable을 사용할 수 있습니다.

    @GetMapping(value = "/variable2/{variable}")
    public String getVariable2(@PathVariable("variable") String str){
        return str;
    }

E. Query Parameter를 이용해서 Get 메소드 구현하기

  • Query Parameter: URL의 ?뒤로 전달하는 파라미터로 있어도 되고 없어도 되는 파라미터에 주로 사용합니다.
https://api.github.com/repos/Qkite/java-algorithm/commits?since=%222022-10-27T00:00:00Z%22

git commit 기록의 시간을 지정할 수 있는 since는 optional하므로 Query Parameter로 구현했습니다.

  • @RequestParam 메소드를 이용해서 Query Patameter를 받았습니다.
    @GetMapping(value = "/request1")
    public String getRequestParam1(@RequestParam String name, @RequestParam String email, @RequestParam String organization){
        return String.format("%s %s %s", name, email, organization);
    }
  • 파라미터가 여러 개인 경우 &를 이용하여 값을 넣어줄 수 있습니다.
~~~/request1?name=hello&email=hello@gmail.com&organization=Greetings

라고 입력을 하면 name에 hello, email에 hello@gmail.com, organization에 Greetings를 입력받고 http body에 hello hello@gmail.com Greetings를 출력합니다.

  • 메소드에 없는 파라미터를 넣는 경우 400 Response error가 발생하게 됩니다.

F. DTO 객체를 활용해서 Get 메소드 구현하기

  • DTO(Data Transfer Object)란?
    : 각 클래스 및 인터페이스에 정보를 전달하기 위해 사용되는 객체입니다. getter과 setter만 가진 클래스로 setter로 인해 값이 변할 수 있습니다.

    class Node{
        private String key;
        private Integer value;

        public Node(String key, Integer value){
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public Integer getValue() {
            return value;
        }
    }
  • DAO(Data Access Object)란?
    : 데이터 베이스의 데이터에 접근하기 위한 객체입니다.
    - CRUD(Create, Read, Update, Delete)
  • VO(Value Object)란?
    : 값으로써 사용되는 객체로 DTO와 유사하지만 값이 변경되지 않는 특징이 있습니다.

  • 코드

값을 받을 수 있는 getter와 값을 변경할 수 있는 Constructor를 가지는 DTO인 MemberDto 클래스를 정의합니다.

{
    private String name;
    private String email;
    private String organization;

    MemberDto(String name, String email, String organization){
        this.name = name;
        this.email = email;
        this.organization = organization;
    }


    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getOrganization() {
        return organization;
    }


    @Override
    public String toString(){
        return String.format("%s %s %s", this.name, this.email, this.organization);
        
    }

}

메소드의 파라미터를 MemberDto로 설정하여 입력된 값을 받고 toString 메소드를 이용해 http body로 출력합니다.

    @GetMapping(value = "/request3")
    public String getRequestParam3(MemberDto memberDto){
        System.out.println(memberDto);
        return memberDto.toString();
    }

2) POST API 만들기

A. POST 메소드란?

  • 주로 새로운 하위 리소스를 생성할 때 사용합니다. 성공적으로 리소르를 생성하면 201 응답코드를 반환합니다.

⭐ POST 메소드와 GET 메소드의 차이점 (면접 단골 문제)

  • GET 메소드: 파라미터를 http request message의 header에 path나 쿼리 파라미터로 전달
  • POST 메소드: HTTP 요청을 할 때 저장하고자 하는 resource를 http request message의 body에 넣어서 전달
    • GET에 비해서 비교적 큰 크기의 정보를 전송할 수 있습니다.
    • HTTP 전송 중 노출되면 안되는 정보(회원가입 시 입력하는 정보 - 비밀번호 등)를 보낼 때 암호화를 하여 POST 메소드로 전송합니다.

B. @RequestMapping으로 POST 메소드 구현하기

    @RequestMapping(value = "/domain", method = RequestMethod.POST)
    public String postExample(){
        return "Hello Post API";
    }

method를 RequestMethod.POST로 지정하여 POST API를 구현합니다.

C. @RequestBody로 POST 메소드 구현하기

POST 요청에서는 HTTP의 body에 json 형식으로 key와 value를 입력하여 정보를 전송할 수 있습니다. 따라서 @RequestBody에 Map<String, Object> 형식의 파라미터를 받습니다.

  • value는 String, Integer, Boolean 등 다양한 형태가 올 수 있으므로 Object로 정의합니다.
    @PostMapping("/member1")
    // request body를 사용함
    public String postMember1(@RequestBody Map<String, Object> postData){
        StringBuilder sb = new StringBuilder();

        postData.entrySet().forEach(map -> {
            sb.append(map.getKey() + " : " + map.getValue() + "\n");
        });

        return sb.toString();
    }

Json(JavaScrpit Object Notation): 자바스크립트의 문법을 따르는 데이터 포맷으로 대체로 네트워크를 통해 데이터를 전달할 때 사용하며, 문자열의 형태로 작성되기 때문에 파싱하기 쉽다는 장점이 있습니다.

3) PUT API 만들기

A. PUT 메소드란?

  • 리소르를 생성하거나 업데이트 하기위해 서버로 데이터를 보내는 역할을 합니다.

B. ResponseEntity를 활용해 PUT 메소드 구현하기

  • ResponseEntity: HttpEntity라는 클래스를 상속받아 구현된 클래스로 사용자의 HTTP Request에 대한 응답 데이터를 가지고 있습니다. HTTP 요청 상태에 해당하는 HTTPStatus, HTTP 요청에 해당하는 HTTPHeaders, HTTP 요청에 대한 응답에 해당하는 HttpBody를 포함합니다.

@ResponseBody에 DTO인 MemberDto 클래스를 파라미터로 받고 return을 ResponseEntity로 구현했습니다. ResponseEntity의 status() 메소드에 HttpStatus.ACCEPTED로 넣어주어 응답 상태를 ACCEPTED로 설정하고, body() 메소드에 memberDto를 넣어주어 응답을 memberDto의 형태로 받게 했습니다.

@RestController
@RequestMapping("/api/v1/put-api")
public class PutController {

    @PutMapping(value = "/member3")
    public ResponseEntity<MemberDto> postMember3(@RequestBody MemberDto memberDto){
        return ResponseEntity
                .status(HttpStatus.ACCEPTED)
                .body(memberDto);
    }
}

따라서 body를 통해 입력 받은 name과 email, organization이 MemberDto에 들어간 후 아래와 같은 json 형태로 출력되고 응답 코드는 202(Accepted)가 뜨게 됩니다.

4) DELETE API 만들기

A. DELETE 메소드란?

  • 서버가 클라이언트로부터 리소스를 식별할 수 있는 값을 받아서 데이터 베이스나 캐시 등의 리소스를 삭제할 때 사용됩니다.

B. @PathVariable를 활용하여 DELETE 메소드 구현하기

Path variable의 형태로 입력받은 데이터를 삭제합니다.

    @DeleteMapping(value = "/{variable}")
    public String deleteVariable(@PathVariable String variable){
        return variable;
    }

C. @RequestParam를 활용하여 DELETE 메소드 구현하기

    @DeleteMapping(value = "/request-delete")
    public String deleteRequestParam(@RequestParam String email, @RequestParam String name){
        return name + ":" + email ;
    }

5) Swagger 도입하기

A. Swagger란?

Swagger는 OAS(Open API Specification)으로 API의 문서화(명세 - 어떤 로직을 수행하는지 설명하고 이 로직을 수행하기 위해서 어떤 값을 요청해야하고 응답값으로 무엇을 받을 수 있는지를 기록)를 자동화 할 수 있도록 도와주고 파라미터를 넣어서 제대로 응답이 오는지 테스트 할 수 있습니다.

B. Swagger 이용해보기

Spring boot 2.7.5, Swagger 3.0.0, Maven 환경에서 진행했습니다.

(1) pom.xml에 의존성 추가

	<dependencies>
...
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-boot-starter</artifactId>
			<version>3.0.0</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>3.0.0</version>
		</dependency>
	</dependencies>

(2) configuration 클래스 추가

@Configuration
public class SwaggerConfiguration {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.OAS_30)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }

}

(3) Controller 생성

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BasicController {

    @GetMapping("/api/hello1")
    public String hello1() {
        return "hello";
    }

    @GetMapping("/api/hello2")
    public String hello2(@RequestParam String param) {
        return param;
    }
}

(4) application.properties를 application.yml로

//application.yml
server:
  port: 8081

NullPointerException: SpringApplication.run에서 NullpointerException이 발생하는 경우 아래 코드 추가하기

@SpringBootApplication
public class HelloApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloApplication.class, args);
        // nullpointerexception 발생
    }
}
//application.yml
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

cf) .yml과 .properties
Spring Boot는 .yml과 .properties과 같은 외부의 설정 파일을 통해서 특정 값을 주입받을 수 있습니다.

.yml은 계층 구조로 표현하고 가독성이 좋습니다. @Value 어노테이션을 이용해 값을 주입할 수 있습니다.

// application.yml
external:
	record-year: 2021
	api:
		name: kakao
		key: 123123
public class ExternalService{
    @Value("${external.record-year}")
	private String recordYear;
  
	@Value("${external.api.name}")
	private String apiName;
  
	@Value("${external.api.key}")
	private Integer apiKey;
}

반면 .properties는 key-value의 형태로 값을 정의할 수 있습니다. .properties에 있는 값도 @Value를 이용해 변수에 주입할 수 있습니다.

이 때 @PropertySource 어노테이션으로 어떤 파일을 사용할 것인지 명시해주고 @Service, @Component, @Configuration과 같은 어노테이션을 통해 클래스를 bean으로 설정해주어야 합니다.

spring.datasource.url=jdbc:mysql://localhost...
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# github token
github.token=ghp_Zwy???...

# slack token
slack.token=xoxb-2402-???...
@PropertySource("classpath:app.properties")
@Component
public class CommitUtil {

    private GitHub github;

    @Value("${github.token}")
    private String token;
}

6) 참고: HTTP response code

A. 1XX (Informational)

서버가 요청을 받았으며 서버에 연결된 클라이언트는 작업을 계속 진행하라는 의미입니다.

  • 100 (Continue): 진행중임을 나타내는 응답코드로 클라이언트가 요청을 계속하고 있는 상황이거나 요청이 이미 완료된 경우 이 응답을 무시해야 함을 나타냅니다.

B. 2XX (Successful)

요청이 성공했음을 나타냅니다.

  • 200 (OK): http 요청이 성공했음을 나타냅니다.
    - GET: 리소스를 가져와 http의 message body에 전달했습니다.
    - HEAD: 표현 헤더(representation headers)가 reponse에 포함되었습니다.
    - PUT 또는 POST: 작업 결과를 설명하는 리소스가 http의 message body에 전송되었습니다.
    - TRACE: 서버에서 수신한 요청 메시지를 message body에 전달되었습니다.

  • 201 (Created): 요청이 성공하여 새로운 리소스가 생성되었음을 나타냅니다. 보통 POST 또는 PUT 요청 후에 전송되는 응답입니다.

  • 202 (Accepted): 요청을 받았지만 아직 처리되지 않았습니다. HTTP에는 나중에 요청 결과를 나타내는 비동기 응답을 보낼 수 있는 방법이 없기 때문에 커밋되지 않습니다. 다른 프로세스나 서버가 요청을 처리하는 경우 또는 일괄 처리를 위한 것입니다.

  • 204 (No Content): 요청을 수행했지만 reponse body가 아예 없는 것을 의미합니다. header에는 유의미한 정보가 담겨있을 수도 있습니다.

    @GetMapping(value = "/request1")
    public void getRequestParam1(@RequestParam String name, @RequestParam String email, @RequestParam String organization){
        System.out.println(String.format("%s %s %s", name, email, organization));

    }

라고 입력했을 때 200 코드가 뜸을 확인할 수 있습니다. 이는 Body에 들어간 정보는 없지만 body 자체는 존재하기 때문입니다.

C. 3XX (Redirection)

요청을 성공적으로 수행하기 위해서 추가적인 조치가 필요함을 의미합니다.

  • 301 (Moved Permanently): 요청한 리소스의 URI 가 변경되었음을 의미합니다.
  • 302 (Found): 요청한 리소스의 URI가 일시적으로 변경되었음을 의미합니다. 향후에 요청을 할 때도 반드시 동일한 URI로 해야합니다.
  • 304 (Not Modified): 브라우저가 서버에 Get 요청을 보낼 때 요청한 정보를 이미 캐시에 가지고 있는 경우 이 데이터가 변경되었는지를 확인합니다. 이 때 이 데이터가 변경되지 않았을 때 304를 내보냅니다.

D. 4XX

잘못된 요청으로 인해 서버에서 요청을 처리할 수 없음을 의미합니다.

  • 400 (Bad Request): 클라이언트의 오류(잘못된 문법 등) 로 인해 서버가 요청을 처리할 수 없는 경우를 의미합니다.

  • 401 (Unautorized): 클라이언트가 승인되지 않아서 요청을 수행할 수 없는 경우를 의미합니다.

  • 403 (Forbidden): 클라이언트가 contents에 접근할 권한이 없는 경우를 의미합니다.

  • 404 (Not Found): 서버가 요청한 리소스를 찾을 수 없다는 것을 의미합니다. URL이 제대로 되지 않은 경우이거나 API에서 endpoint는 유효하지만 리소스 자체가 존재하지 않는 경우에 발생할 수 있습니다.

  • 405 (Method Not Allowd): 해당하는 리소스에 request method가 적합하지 않은 경우 발생합니다. 예를 들면 @GetMapping를 붙인 메소드를 Post 메소드를 이용해 호출할 때 발생합니다.

  • 408 (Request Timeout): 일정 시간 동안 클라이언트의 요청이 없었지만 서버가 연결되어있는 경우 사용하지 않는 서버의 연결을 종료하려고 함을 의미합니다.

  • 409 (Conflict): 요청이 서버의 현재 상태와 충돌함을 의미합니다.

  • 410 (Gone): 요청한 컨텐츠가 서버에서 영구적으로 삭제된 상태일 때 발생합니다. 클라이언트는 캐시와 리소스 링크를 제거해야합니다.

  • 413 (Payload Too large): 요청한 엔티티가 제한된 범위를 넘어갈 때를 의미합니다. 예를 들어 매우 큰 파일을 업로드 할 때 발생할 수 있습니다.

E. 5XX

서버에서 오류가 발생했음을 의미합니다.

  • 500 (Internal Server Error): 서버가 요청을 제대로 수행할 수 없음을 의미합니다. 다른 정확한 에러로 분류되지 않는 예외적인 에러를 나타낸다고 볼 수 있습니다.
  • 502 (Bad Gateway): 서버가 다른 서버로 부터 잘못된 응답을 받았음을 의미합니다. 게이트 웨이가 잘못된 프로토콜을 연결하거나 특정 서버가 과부하 되는 등 다양한 경우에서 발생할 수 있습니다.
  • 503 (Service Unavailable): 서버가 요청을 받을 준비가 되지 않은 경우를 의미합니다.
  • 504 (Gateway Timeout): 서버가 제시간 안에 응답을 받지 못했음을 의미합니다.

그 외의 코드가 궁금한 경우 https://developer.mozilla.org/en-US/docs/Web/HTTP/Status 를 참고해주세요


출처
1. 스프링부트 핵심가이드, 장정우, pp.55-80
2. RequestMapping 어노테이션 공식 문서: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html
3. http method 설명: https://velog.io/@yh20studio/CS-Http-Method-%EB%9E%80-GET-POST-PUT-DELETE
4. DAO, DTO, VO: https://melonicedlatte.com/2021/07/24/231500.html
5. Swagger: https://velog.io/@wotj7687/Spring-Boot-Swagger-3.0.0-%EC%A0%81%EC%9A%A9
6. .yml vs .properties (1): https://velog.io/@tjswlsdl135/application.properties-vs-application.yml
7. .yml vs .properties (2): https://aeliketodo.tistory.com/23
8. http response code 설명(1): https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
9. http response code 설명(2): https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

0개의 댓글