EC2 인스턴스 화면으로 돌아와서 IAM에 생성한 역할을 저장한다 - 이제 EC2는 S3, CodeDeploy에 접근할 수 있다
EC2 인스턴스가 적절한 보안 그룹을 갖고 있는지 확인한다
EC2 대시 보드에서 인스턴스로 이동한다
보안 탭을 클릭한다
보안 그룹을 클릭해 이동한다
보안그룹의 인바운드 규칙에 8080과 443 포트가 포함되어 있는지 확인한다 - 포함되어있지 않다면, [ 인바운드 규칙 편집 ] 버튼을 클릭해 수정 화면으로 이동하여 추가한다 - 8080번 포트는 서버 배포를 위해 필요한다 - 443 포트는 CodeDeploy-Agent의 정상적인 작동을 위해 필요하다
사용자 지정 TCP에 - 포트범위를 8080으로 - 소스를 위치 무관으로 추가한다
HTTPS 유형에서 - 소스를 위치 무관으로 추가한다
'규칙 저장' 을 클릭한다
저장 후 22, 8080, 443 포트가 포함되어 있는지 확인한다
EC2 인스턴스의 세팅이 완료되었다
2. 부여받은 IAM User 권한과 리소스를 활용하여 파이프라인 구축
1) 배포 자동화를 위한 EC2 인스턴스의 역할(IAM Role) 확인
EC2 인스턴스에는 태그와 IAM Role이 설정되어 있다
역할이 가지고 있는 권한은 아래와 같다 - EC2 인스턴스의 Name은 해당 인스턴스에 Name을 키로 갖는 태그 값과 동일하다 - 별도의 태그를 추가하지 않고, 이후에도 이 인스턴스의 Name 태그 값을 이용해 검색한다
EC2 인스턴스에 연결되어있는 IAM 역할은 아래 이미지와 같은 권한을 가지고 있다 - 추가되어있는 권한과 신뢰관계가 'EC2 인스턴스에 태그와 역할 부여'에서 진행한 내용과 동일한지 확인한다
발급받은 EC2 인스턴스를 통해 연결되어있는 보안 그룹을 클릭해 이동한다
모든 인스턴스에 동일한 보안그룹이 적용되어 있다 - 보안그룹 인바운드 규칙이 'EC2 인스턴스에 태그와 역할 부여'에서 진행한 내용과 동일한지 확인한다
3. EC2를 활용한 파이프라인 구축
1) Local 환경 설정
구축에 활용할 로컬 환경의 'be-sprint-deployment/DeployServer' 경로에 appspec.yml 파일을 추가한다 - appspec.yml은 배포 자동화를 도와주는 CodeDeploy-Agent가 인식하는 파일이다
애플리케이션과 REST API 통신을 하는 프런트엔드(Frontend) 쪽 웹 앱이 아래 그림과 같다
일반적으로 프런트엔드의 웹 사이트에서는 자바스크립트를 통해서 사용자가 입력하는 입력 폼 필드의 값에 대한 유효성 검증을 진행한다
프런트엔드에서 검증에 통과되었다고 그 값이 반드시 유효한 값이라고 보장할 수는 없다 - 유효성 검사를 통과한 뒤 [등록] 버튼을 누르면 서버 쪽으로 HTTP Request 요청이 전송된다 - 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용하면 브레이크 포인트(break point)를 이용해서 값을 조작할 수 있다 - 프런트엔드에서 유효성 검사를 진행해도 서버에서 한번 더 유효성 검사를 진행해야 한다 - 프런트엔드에서 진행하는 유효성 검사는 사용자 편의성을 위해 진행하는 절차이다
2. DTO 클래스에 유효성 검증 적용
MemberController에서 사용된 MemberPostDto 클래스와 MemberPatchDto 클래스에 유효성 검증을 적용해 본다
1) 유효성 검증을 위한 의존 라이브러리 추가
DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요하다
build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가한다
//Spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성이 분리되서 @validation 사용을 하기 위해 추가 해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
2) MemberPostDto 유효성 검증 제약 사항
email (이메일 주소) - 값이 비어있지 않거나 공백이 아니어야 한다 - 유효한 이메일 주소 형식이어야 한다
name (이름) - 값이 비어있지 않거나 공백이 아니어야 한다
phone (휴대폰 번호) - 값이 비어있지 않거나 공백이 아니어야 한다 - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다 : 010-1234-5678
아래는 유효성 검증을 위해 제약사항을 적용한 코드이다 - 회원 등록을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 emil, name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class MemberPostDto {
@NotBlank(message = "공백없이 작성해 주시기 바랍니다")
@Email
private String email;
@NotBlank(message = "공백없이 작성해 주시기 바랍니다")
private String name;
@NotBlank(message = "공백없이 작성해 주시기 바랍니다")
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
private String phone;
private long memberId;
public String getEmail() {
return email;
}
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
//--------------------------------- 회원정보 Dto class 작성
//--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
public void setEmail(String email) {
this.email = email;
}
public void setName(String name) {
this.name = name;
}
public void setPhone(String phone) {
this.phone = phone;
}
public long getMemberId(){
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
}
2-1) MemberPostDto의 멤버 변수에 적용된 유효성 검증 내용
email - @NotBlank : 이메일 정보가 비어있지 않은지를 검증한다 : null 값이나 공백(””), 스페이스(” “) 같은 값들을 허용하지 않는다 : 유효성 검증에 실패하면 에러 메시지를 콘솔에 출력한다
요청으로 전달 받는 MemberPostDto 클래스의 각 멤버 변수에 유효성 검증을 위한 애너테이션을 추가하여 MemberController의 핸들러 메서드에 별도의 유효성 검증을 추가하지 않고 유효성 검증 로직을 분리하였다
2-2) Controller에 유효성 검증 적용
유효성 검증 애너테이션이 추가된 DTO 클래스에서 유효성 검증 로직이 실행되게 하기 위해서 postMember()와 같이 DTO 클래스에 @Valid 애너테이션을 추가한다
@PostMapping
//Dto에 유효성 검증 조건을 적용하였으므로 @Valid 를 적용하여 준다
public ResponseEntity postMember (@Valid @RequestBody MemberPostDto memberPostDto) {
return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
}
postman에서 유효하지 않은 정보를 전송하면 Error이 발생한다
Spring MVC에서 출력한 로그를 IntelliJ IDE에서 확인하면 아래와 같이 출력된다 - email의 오류를 출력한다 - name는 공백이 없으므로 정상적으로 전송된다 - phone은 '010'이 아니므로 오류 메시지를 출력한다
3)MemberPatchDto 유효성 검증 제약 사항
name (이름) - 값이 비어있지 않거나 공백이 아니어야 한다
phone (휴대폰 번호) - 값이 비어있지 않거나 공백이 아니어야 한다 - 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 한다 : 010-1234-5678
아래는 유효성 검증을 위해 제약사항을 적용한 코드이다 - 회원 정보 수정을 위해 클라이언트에서 전달 받는 Request Body의 데이터인 name, phone 정보에 유효성 검증을 위한 애너테이션을 추가하였다
import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
public class MemberPatchDto {
@Email
private String email;
@Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
private String name;
@Pattern(regexp = "^(?=\\s*\\S).*$", message = "공백이 아니어야 합니다")
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "010으로 시작하는 11자리 숫자를 '-'을 넣어서 작성해 주시기 바랍니다")
private String phone;
private long memberId;
public String getEmail() {
return email;
}
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
//--------------------------------- 회원정보 Dto class 작성
//--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
public void setEmail(String email) {
this.email = email;
}
public void setName(String name) {
this.name = name;
}
public void setPhone(String phone) {
this.phone = phone;
}
public long getMemberId(){
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
}
3-1) MemberPatchDto 클래스의 멤버 변수에 적용된 유효성 검증 내용
memberId - Request Body에 포함되는 데이터가 아니므로 유효성 검증이 필요하지 않다
name - @Pattern : 정규 표현식으로 다음 내용을 체크한다 -> 이름 정보가 비어있으면 검증에 성공 -> 이름 정보가 비어 있지 않고, 공백 문자열이라면 검증에 실패
phone - @Pattern : 정규 표현식으로 다음 내용을 체크한다 -> 휴대폰 정보가 비어있으면 검증에 성공 -> 휴대폰 정보가 비어 있지 않고, 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이 아니라면 검증에 실패
MemberPostDto 클래스와 달리 MemberPatchDto 에서는 모두 정규 표현식을 사용했다
웹 브라우저에서 회원 가입 후에 이름(또는 닉네임)만 수정하거나 휴대폰 번호만 수정할 수도 있고 모두 수정할 수도 있다
이름이나 휴대폰 번호의 값으로 공백 문자열(”” 또는 “ “)만 포함이 되어 있을 경우를 검증해야할 수 있다
3-2) Controller 클래스에 patchMember() 핸들러 메서드의 코드 적용
@PatchMapping("/{member-id}")
public ResponseEntity patchMember (@PathVariable("member-id")long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// memberPatchDto.setName("홍길동");
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
3. 쿼리 파라미터(Query Parameter 또는 Query String) 및 @Pathvariable에 대한 유효성 검증
Request Body의 유효성 검사 검증 대상에서 빠진 항목이 있다
patchMember() 핸들러 메서드의 URI path에서 사용되는 @PathVariable("member-id") long memberId 변수이다
일반적으로 수정이 필요한 데이터의 식별자는 0이상의 숫자로 표현을 한다
patchMember() 핸들러 메서드에서 사용되는 memberId에 "1 이상의 숫자여야 한다"라는 조건을 부여하도록 한다 - 1 이상의 숫자일 경우에만 유효성 검증에 통과하도록 @Min(1) 이라는 검증 애너테이션을 추가한다
@PatchMapping("/{member-id}")
public ResponseEntity patchMember (@PathVariable("member-id") @Min(1) long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// memberPatchDto.setName("홍길동");
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
위와 같이 수정하여도 Postman으로 URI path에 유효하지 않은 값을 전송하면 유효성 검증이 정상적으로 수행되지 않는다
유효성 검증이 정상적으로 수행되려면 클래스 레벨에 @Validated 애너테이션을 붙여주어야 한다
postman으로 실행해 보면 응답 결과는 Response Satus가 500인 ‘Internal Server Error’를 전달 받는다
@Request Body에 대한 유효성 검증 실패 메시지와 조금 다르지만 유효성 검증은 정상적으로 이루어진 것이다
4. Jakarta Bean Validation
DTO 클래스의 유효성 검증을 위해서 사용한 애너테이션은 Jakarta Bean Validation라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션이다
Jakarta Bean Validation은 라이브러리처럼 사용할 수 있는 API가 아닌 스펙(사양, Specification) 자체이다 - Jakarta Bean Validation은 일종의 기능 명세와 같다 - Jakarta Bean Validation 스펙을 구현한 구현체가 Hibernate Validator이다
Jakarta Bean Validation의 애너테이션을 DTO 클래스에만 사용할 수 있는 것은 아니다 - Java Bean 스펙을 준수하는 Java 클래스라면 Jakarta Bean Validation의 애너테이션으로 유효성 검증을 할 수 있다
5. Custom Validator를 사용한 유효성 검증
DTO 클래스에 유효성 검증 시 Jakarta Bean Validation에 내장된 애너테이션 중에 목적에 맞는 애너테이션이 없을 수 있다 - 목적에 맞는 애너테이션을 직접 만들어서 유효성 검증에 적용할 수 있다
MemberPatchDto 클래스의 name과 phone 멤버 변수에서 공백 여부를 검증하는 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션을 Custom Validator를 사용하여 바꿔 본다
정규 표현식(Regular Expression)은 성능적인 면에서 때로는 비싼 비용을 치뤄야 될 가능성이 있다 - 모든 로직을 정규표현식 위주로 작성하는 것은 좋은 개발 방식이 아니다
1) Custom Validator를 구현하기 위한 절차
Custom Validator를 사용하기 위한 Custom Annotation을 정의한다 - 공백을 허용하지 않는 Custom Annotation 'Notspace'를 생성한다 - 중요한 부분은 NotSpace 애너테이션을 멤버 변수에 추가할 경우 동작하는 Custom Validator를 추가하는 것이다 : @Constraint(validatedBy = {NotSpaceValidator.class}) - 유효성 검증 실패 시 표시되는 디폴트 메시지를 애너테이션에서 정의해 준다 : String message() default "공백이 아니어야 합니다" : 애너테이션에서 정의한 디폴트 메시지는 적용 시 공통적으로 사용된다
정의한 Custom Annotation에 바인딩 되는 Custom Validator를 구현한다 - CustomValidator를 구현하기 위해서는 ConstraintValidator 인터페이스를 먼저 구현해야 한다 - ConstraintValidator<NotSpace, String>에서 : NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미한다 : String은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미한다
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class NotSpaceValidator implements ConstraintValidator<Notspace, String> {
@Override
public void initialize(Notspace constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || StringUtils.hasText(value);
}
}
유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다 - name과 phone 등의 멤버 변수에서 적용된 @Pattern(regexp = "^(?=\\s*\\S).*$") 애너테이션 대신 Custom Annotation인 @NotSpace를 추가한다 - 애플리케이션을 재시작하고 patchMember() 핸들러 메서드에 해당되는 URI로 요청을 전송하여 확인한다
DTO는 Data Transfer Object의 약자로 마틴 파울러(Martin Fowler)가 ‘Patterns of Enterprise Application Architecture’ 라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍처 패턴이다
Transfer라는 의미에서 알 수 있듯이 데이터를 전송하기 위한 용도의 객체이다
데이터 전송은 클라이언트에서 서버 쪽으로 전송하는 요청 데이터, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터의 형식으로 클라이언트와 서버 간에 데이터 전송이 이루어진다
2. DTO를 사용하는 이유
1) 코드의 간결성
public class MemberController {
@PostMapping
public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
@RequestParam("email")String email,
@RequestParam("name")String name,
@RequestParam("phone")String phone) {
// 코드(1)
// System.out.println("# email: " + email);
// System.out.println("# name: " + name);
// System.out.println("# phone: " + phone);
// JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
// Map<String, String> map = new HashMap<>();
// map.put("email", email);
// map.put("name", name);
// map.put("phone", phone);
//코드(2)
//@RequestHeader Map을 사용하여 전체 헤더 정보를 받아 온다
for (Map.Entry<String, String> entry : headers.entrySet()){
System.out.println("key: " + entry.getKey() +
", value: " + entry.getValue());
}
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
// 코드(1)
// return new ResponseEntity<>(map, HttpStatus.CREATED);
//코드(2)
//@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);
코드(1)에서 작성한 MemberController의 postMember() 핸들러 메서드에서 개선해야 될 부분을 찾아본다 - 코드(1)에서는 회원 정보를 저장하기 위해서 총 세 개의 @RequestParam 애너테이션을 사용하고 있다 - 요청 데이터는 회원의 주소 정보, 로그인 패스워드, 패스워드 확인 정보 등 추가 내용이 회원 정보에 포함될 수 있다 - postMember()에 파라미터로 추가되는 @RequestParam의 개수는 늘어날 수 밖에 없다
클라이언트의 요청 데이터를 하나의 객체로 모두 전달 받을 수 있다면 코드 자체가 간결해질 수 있다 - DTO 클래스는 요청 데이터를 하나의 객체로 전달 받는 역할을 한다
// public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
// @RequestParam("email")String email,
// @RequestParam("name")String name,
// @RequestParam("phone")String phone) {
//코드(3)
public class ResponseEntity postMember(MemberDto memberDto){
로 변경하면
//코드(2)
//@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
// return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);
//코드(3)
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
으로 변경된다
postMember()에서 @RequestParam을 사용하는 부분이 사라지고 MemberDto memberDto 를 추가한다
비즈니스 로직이 없는 상태이지만 - @RequestParam을 통해 전달 받은 요청 데이터들을 Map에 추가하는 로직을 삭제하고 - MemberDto 객체를 ResponseEntity 클래스의 생성자 파라미터로 전달하도록 변경하였다
일단, 코드는 매우 간결해졌다
2) 데이터 유효성(Validation) 검증의 단순화
지금까지 작성한 Controller의 핸들러 메서드는 클라이언트의 요청 데이터에 대한 유효성 검증 작업을 거치지 않았다
클라이언트 쪽에서 회원 정보의 email 주소를 ‘1111@gmail.com’ 같은 이메일 주소 형식이 아닌 ‘1111’ 같은 단순 문자열로 전송해도 정상적으로 핸들러 메서드 쪽에서 전달 받을 수 있다
서버 쪽에서 유효한 데이터를 전달 받기 위해 데이터를 검증하는 것을 유효성(Validation)검증이라고 한다
@RestController
@RequestMapping("/no-dto-validation/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
// (1) email 유효성 검증
if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
throw new InvalidParameterException();
}
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
(1)은 정규 표현식을 사용해서 이메일 주소의 유효성을 검증하는 로직으로 핸들러 메서드 내에 직접적으로 포함되어 있다
name, phone에 대한 유효성 검증도 필요하면 핸들러 내의 코드는 유효성을 검증하는 로직들로 코드가 복잡해 진다
HTTP 요청을 전달 받는 핸들러 메서드는 요청을 전달 받는 것이 주 목적이기 때문에 최대한 간결하게 작성되는 것이 좋다
핸들러 메서드 내부에 있는 유효성 검사 로직을 외부로 뺄 수 있다면 핸들러 메서드의 간결함을 유지할 수 있을 것이다
DTO 클래스를 사용하면 유효성 검증 로직을 DTO 클래스로 빼내어 핸들러 메서드의 간결함을 유지할 수 있다
import javax.validation.constraints.Email;
public class MemberDto {
//MemberDto의 email 멤버 변수에 유효성 검증을 적용한다
//email 멤버 변수에 @Email을 추가하여 클라이언트의 요청 데이터에 유효한 이메일 주소가 포함되어 있지 않을 경우
//유효성 검증에 실패하므로 클라이언트의 요청은 거부(reject)된다
//MemberDto 클래스에서 이메일에 대한 유효성 검증을 진행하므로
//MemberController의 postMember() 핸들러 메서드는 코드(5)와 같이 간결해진다
@Email //'org.springframework.boot:spring-boot-starter-validation'을 추가해 주어야 한다
private String email;
private String name;
private String phone;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
코드(5)가 적용된 MemberController class 코드이다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
public class MemberController {
@PostMapping
// 코드(1)
// public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
// @RequestParam("email")String email,
// @RequestParam("name")String name,
// @RequestParam("phone")String phone) {
//코드(3)
// public class ResponseEntity postMember(MemberDto memberDto){
//코드(5)
public ResponseEntity postMember (@Valid/*'org.springframework.boot:spring-boot-starter-validation'을 추가해 주어야 한다*/MemberDto memberDto) {
// 코드(1)
// System.out.println("# email: " + email);
// System.out.println("# name: " + name);
// System.out.println("# phone: " + phone);
// JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
// Map<String, String> map = new HashMap<>();
// map.put("email", email);
// map.put("name", name);
// map.put("phone", phone);
//코드(4)
//email 유효성 검증 : 코드(1) @RequestParam 과 함게 사용 시 에러 없음
// if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
// throw new InvalidParameterException();
// }
// Map<String, String> map = new HashMap<>();
// map.put("email", email);
// map.put("name", name);
// map.put("phone", phone);
//코드(2)
//@RequestHeader Map을 사용하여 전체 헤더 정보를 받아 온다
// for (Map.Entry<String, String> entry : headers.entrySet()){
// System.out.println("key: " + entry.getKey() +
// ", value: " + entry.getValue());
// }
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
// 코드(1)
// return new ResponseEntity<>(map, HttpStatus.CREATED);
//코드(2)
//@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
// return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);
//코드(3)
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
// 코드(1)
// String reponse =
// "{\"" +
// "email\":\""+email+"\"," +
// "\"name\":\""+name+"\",\"" +
// "phone\":\"" + phone+
// "\"}";
// return reponse;
}
@Valid 애너테이션은 MemberDto 객체에 유효성 검증을 적용하게 해주는 애너테이션이다 - @Valid를 사용하기 위해서는 build.gradle에 아래 코드를 적용해야 한다
implementation 'org.springframework.boot:spring-boot-starter-validation'
Spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성이 분리되서
@validation 사용을 하기 위해 추가 해줘야 한다.
DTO 클래스를 사용하는 가장 중요한 목적은 - 비용이 많이 드는 작업인 HTTP 요청의 수를 줄이고 - 도메인 객체와 분리하는 것이다
3. HTTP 요청/응답 데이터에 DTO 적용하기
MemberController의 핸들러 메서드에 DTO 클래스를 적용해 본다
지금까지는 HTTP Request Body가 JSON 형식이 아닌 ‘x-www-form-urlencoded’ 형식의 데이터였다
프런트엔드와 함께 프로젝트를 진행 또는 하나의 제품이나 서비스를 제작할 경우 - 프런트엔드의 웹앱과 백엔드의 애플리케이션 간 기본 API 통신 프로토콜은 대부분 JSON 형식으로 한다
클라이언트에서 전달되는 요청 데이터 중에서 바디에 해당되는 데이터를 Request Body라고 한다
@RequestBody 애너테이션의 추가에 따라 Request Body를 전달 받는 방식이 JSON 형식으로 변경된다
1) DTO 클래스 적용을 위한 코드 리팩토링 절차
회원 정보를 전달 받을 DTO 클래스를 생성한다 - MemberCotroller에서 현재 회원 정보로 전달 받는 각 데이터 항목(email, name, phone)들을 DTO 클래스의 멤버 변수로 추가한다
클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾는다 - Request Body가 필요한 핸들러는 HTTP POST, PATCH, PUT 같이 리소스의 추가나 변경이 발생하는 경우이다 - HTTP GET은 리소스를 조회하는 용도이기 때문에 Request Body는 필요가 없다 - @PostMapping, @PatchMapping 애너테이션이 붙은 핸들러 메서드를 찾는것과 동일하다
@RequestParam의 코드를 DTO 클래스의 객체로 수정한다
Map 객체로 작성되어 있는 Response Body를 DTO 클래스의 객체로 변경한다
import javax.validation.constraints.Email;
public class MemberDto {
@Email
private String email;
private String name;
private String phone;
private long memberId;
public String getEmail() {
return email;
}
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
//--------------------------------- 회원정보 Dto class 작성
//--------------------------------- RequestBody를 전달받기 위한 Dto class 작성
public void setEmail(String email) {
this.email = email;
}
public void setName(String name) {
this.name = name;
}
public void setPhone(String phone) {
this.phone = phone;
}
public long getMemberId(){
return memberId;
}
public void getMemberId(long memberId) {
this.memberId = memberId;
}
}
위의 코드는 회원 정보 등록과 수정 시 Request Body를 전달 받기 위한 Dto 클래스이다
DTO 클래스를 만들 때에는 멤버 변수 이외에 각 멤버 변수에 해당하는 getter 메서드가 있어야 한다 - getter 메서드가 없으면 Response Body에 해당 멤버 변수의 값이 포함되지 않는 문제가 발생한다 - setter 메서드는 개발자의 필요에 의해서 생성할 수 있다
※ lombok 라이브러리를 등록하면 getter/setter 메서드가 내부에서 자동으로 만들어진다
@RequestParam을 사용했던 코드를 DTO 클래스를 사용하여 수정하였다 - postMember()에서는 MemberPostDto 클래스의 객체를 - patchMember()에서는 MemberPatchDto 클래스의 객체를 통해 Request Body를 한번에 전달 받을 수 있도록 하였다
@RequestParam을 사용했던 코드가 간결해진 것을 확인할 수 있다
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
public class MemberController {
@PostMapping
public ResponseEntity postMember (@RequestBody MemberPostDto memberPostDto) {
return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
}
//추가 코드 : patchMember
@PatchMapping("/{member-id}")
public ResponseEntity patchMember (@PathVariable("member-id")long memberId, @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
memberPatchDto.setName("홍길동");
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id")long memberId, MemberGetDto memberGetDto) {
System.out.println("# memberId: " + memberId); //터미널에 출력되는 코드이다
// not implementation
return new ResponseEntity<>(memberGetDto, HttpStatus.OK);
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다
public ResponseEntity getMembers(MemberGetDto memberGetDto) {
System.out.println("# get Members"); //터미널에 출력되는 코드이다
// not implementation
return new ResponseEntity<>(memberGetDto, HttpStatus.OK);
}
//추가 코드 : getDelete
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id")long memberId) {
// No need business logic
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
postman을 통해 post를 보내보면 아래와 같이 데이터가 입력되는 것을 확인할 수 있다
4. Annotation
1) @RequestBody
JSON 형식의 Request Body를 Dto 클래스의 객체로 변환을 시켜주는 역할을 한다 - 클라이언트에서 전송하는 Request Body가 JSON 형식이어야 하기 때문이다
JSON 형식이 아닌 다른 형식의 데이터를 전송하면 Spring 내부에서 ‘Unsupported Media Type’ 과 같은 에러 메시지를 포함한 응답을 전달한다
2) @ResponseBody
JSON 형식의 Response Body를 클라이언트에게 전달하기 위해 DTO 클래스의 객체를 Response Body로 변환한다
postMember(), patchMember()에는 @ResponseBody 를 사용하는 곳이 없다 - postMember(), patchMember() 핸들러 메서드의 리턴 값이 ResponseEntity 클래스의 객체이기 때문이다
Spring MVC에서는 핸들러 메서드에 @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity일 경우, 내부적으로 HttpMessageConverter가 동작하게 되어 응답 객체(여기서는 DTO 클래스의 객체)를 JSON 형식으로 바꿔준다
3) JSON 직렬화(Serialization)와 역직렬화(Deserialization)
클라이언트에서 JSON 형식의 데이터를 서버 쪽으로 전송하면 서버의 웹 애플리케이션은 전달 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는 기능을 역직렬화(Deserialization)이라고 한다
서버에서 클라이언트에게 응답 데이터를 전송하기 위해 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것을 직렬화(Serialization)라고 한다
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample01{
public static void main(String[] args) {
/*기본적으로 RestTemplate의 객체를 생성하기 위해서는
RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/
RestTemplate restTemplate = new RestTemplate(
/*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/
new HttpComponentsClientHttpRequestFactory());
//URI 생성
/*
UriComponentsBuilder 클래스를 이용해서 UriComponents 객체를 생성하고
UriComponents 객체를 이용해서 HTTP Request를 요청할 엔드포인트의 URI를 생성한다
*/
UriComponents uriComponents = UriComponentsBuilder
.newInstance() //UriComponentsBuilder 객체를 생성한다
.scheme("http") //URI의 scheme을 설정한다
.host("worldtimeapi.org") //호스트 정보를 입력한다
.port(80) //디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능하다
.path("api/timezone/{continents}/{city}")
/*
URI의 경로(path)를 입력한다
URI의 path에서 {continents}, {city} 두 개의 템플릿 변수를 사용하고 있다
두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서
expand() 메서드 파라미터의 문자열로 채워진다
빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다.
*/
.encode()
/*URI에 사용된 템플릿 변수들을 인코딩 한다
non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미이다
*/
.build();
//UriComponents 객체를 생성한다
URI uri = uriComponents.expand("Asia","Seoul").toUri();
/* expand 메서드는 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체한다
toUri 메서드는 객체를 생성한다
*/
}
6. Request 전송
엔드포인트로 Request를 전송한다
getForObject(URI uri, Class<T> responseType) - getForObject() 메서드는 HTTP Get 요청을 통해 서버의 리소스를 조회한다 - URI uri : Request를 전송할 엔드포인트의 URI 객체를 지정한다 - Class responseType : 응답으로 전달 받을 클래스의 타입을 지정한다
package com.dreamfactory.restAPI;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample01{
public static void main(String[] args) {
/*기본적으로 RestTemplate의 객체를 생성하기 위해서는
RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야 한다*/
RestTemplate restTemplate = new RestTemplate(
/*HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다*/
new HttpComponentsClientHttpRequestFactory());
//URI 생성
/*
UriComponentsBuilder 클래스를 이용해서 UriComponents 객체를 생성하고
UriComponents 객체를 이용해서 HTTP Request를 요청할 엔드포인트의 URI를 생성한다
*/
UriComponents uriComponents = UriComponentsBuilder
.newInstance() //UriComponentsBuilder 객체를 생성한다
.scheme("http") //URI의 scheme을 설정한다
.host("worldtimeapi.org") //호스트 정보를 입력한다
.port(80) //디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능하다
.path("api/timezone/{continents}/{city}")
/*
URI의 경로(path)를 입력한다
URI의 path에서 {continents}, {city} 두 개의 템플릿 변수를 사용하고 있다
두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서
expand() 메서드 파라미터의 문자열로 채워진다
빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다.
*/
.encode()
/*URI에 사용된 템플릿 변수들을 인코딩 한다
non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미이다
*/
.build();
//UriComponents 객체를 생성한다
URI uri = uriComponents.expand("Asia","Seoul").toUri();
/* expand 메서드는 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체한다
toUri 메서드는 객체를 생성한다
*/
//Request 전송
//getForObject() 메서드로 HTTP Get 요청을 통해 서버의 리소스를 조회한다
//응답 데이터를 문자열로 받을 수 있도록 String.class로 지정한다
String result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
코드를 최종적으로 실행하면 아래와 같이 출력된다
1) getForObject()를 이용하여 커스텀 클래스 타입으로 원하는 정보만 응답으로 전달 받기
전달 받고자하는 응답 데이터의 JSON 프로퍼티 이름과 클래스의 멤버변수 이름이 동일해야 한다
해당 멤버 변수에 접근하기 위한 getter 메서드 역시 동일한 이름이어야 한다
package com.dreamfactory.restAPI;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample02 {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate(
new HttpComponentsClientHttpRequestFactory()
);
UriComponents uriComponents = UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
.port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
//WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);
System.out.println("# datatime: " + worldTime.getDatetime());
System.out.println("# timezone: " + worldTime.getTimezone());
System.out.println("# day_of_week: " + worldTime.getDay_of_week());
}
}
WorldTime에 대한 클래스를 새로 생성해야 한다
package com.dreamfactory.restAPI;
public class WorldTime {
private String datetime;
private String timezone;
private String day_of_week;
public String getDatetime() {
return datetime;
}
public String getTimezone() {
return timezone;
}
public String getDay_of_week() {
return day_of_week;
}
}
실행하면 지정된 데이터만 응답 전송되는 것을 확인할 수 있다
2) getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기
응답 데이터는 ResponseEntity 클래스로 래핑되어서 전달 된다
getBody(), getHeaders() 메서드 등을 이용해서 바디와 헤더 정보를 얻을 수 있다
응답으로 전달 되는 모든 헤더 정보를 보고 싶다면 getHeaders().entrySet() 메서드를 이용해서 확인할 수 있다
package com.dreamfactory.restAPI;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample02 {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate(
new HttpComponentsClientHttpRequestFactory()
);
UriComponents uriComponents = UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
.port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
//WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
// WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);
// System.out.println("# datatime: " + worldTime.getDatetime());
// System.out.println("# timezone: " + worldTime.getTimezone());
// System.out.println("# day_of_week: " + worldTime.getDay_of_week());
//getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기
ResponseEntity<WorldTime> response = restTemplate.getForEntity(uri, WorldTime.class);
System.out.println("# datatime: " + response.getBody().getDatetime());
System.out.println("# timezone: " + response.getBody().getTimezone());
System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
System.out.println("# HTTP Status Code: " + response.getStatusCode());
System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
System.out.println("# Content Type: " + response.getHeaders().getContentType());
System.out.println(response.getHeaders().entrySet());
}
}
실행하면 아래와 같은 결과 값이 출력된다
3) exchange() 를 사용한 응답 데이터 받기
exchange() 메서드를 사용한 방식은 일반적인 HTTP Request 방식입니다.
HTTP Method나 HTTP Request, HTTP Response 방식을 개발자가 직접 지정해서 유연하게 사용할 수 있다
exchange(URI uri, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) - HTTP Method, RequestEntity, ResponseEntity를 직접 지정해서 HTTP Request를 전송할 수 있는 가장 일반적인 방식이다 - URI uri : Request를 전송할 엔드포인트의 URI 객체를 지정한다 - HttpMethod method : HTTP Method 타입을 지정한다 - HttpEntity<?> requestEntity : HttpEntity 객체를 지정하며, HttpEntity 객체를 통해 헤더 및 바디, 파라미터 등을 설정해줄 수 있다 - Class<T> responseType : 응답으로 전달 받을 클래스의 타입을 지정한다
package com.dreamfactory.restAPI;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample02 {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate(
new HttpComponentsClientHttpRequestFactory()
);
UriComponents uriComponents = UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
.port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
//WorldTime 클래스를 사용해서 전체 응답 데이터가 아니라 datetime과 timezone 정보만 전달 받는다
// WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);
// System.out.println("# datatime: " + worldTime.getDatetime());
// System.out.println("# timezone: " + worldTime.getTimezone());
// System.out.println("# day_of_week: " + worldTime.getDay_of_week());
//getForEntity()를 사용한 Response Body(바디, 컨텐츠) + Header(헤더) 정보 전달 받기
// ResponseEntity<WorldTime> response = restTemplate.getForEntity(uri, WorldTime.class);
// System.out.println("# datatime: " + response.getBody().getDatetime());
// System.out.println("# timezone: " + response.getBody().getTimezone());
// System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
// System.out.println("# HTTP Status Code: " + response.getStatusCode());
// System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
// System.out.println("# Content Type: " + response.getHeaders().getContentType());
// System.out.println(response.getHeaders().entrySet());
//exchange() 를 사용한 응답 데이터 받기
ResponseEntity<WorldTime> response = restTemplate.exchange(
uri, HttpMethod.GET, null, WorldTime.class);
System.out.println("# datatime: " + response.getBody().getDatetime());
System.out.println("# timezone: " + response.getBody().getTimezone());
System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
System.out.println("# HTTP Status Code: " + response.getStatusCode());
System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
}
}
General headers - 메시지 전체에 적용되는 헤더이며, body를 통해 전송되는 데이터와는 관련이 없는 헤더이다
Response headers - 위치 또는 서버 자체에 대한 정보(이름, 버전 등)와 같이 응답에 대한 부가적인 정보를 갖는 헤더이다 - Vary, Accept-Ranges와 같이 상태 줄에 넣기에는 공간이 부족했던 추가 정보를 제공한다
Representation headers - Entity headers라고 부르기도 한다 - body에 담긴 리소스의 정보(콘텐츠 길이, MIME 타입 등)를 포함하는 헤더이다
2. Spring MVC에서 HTTP Header의 사용
1) 클라이언트와 서버 관점에서의 대표적인 HTTP 헤더
클라이언트와 서버의 관점에서 내부적으로 가장 많이 사용되는 헤더 정보로 ‘Content-Type’이 있다 - 클라이언트와 서버가 주고 받는 HTTP 메시지 body의 데이터 형식을 알려준다 - 클라이언트와 서버는 Content-Type의 데이터 형식에 맞는 데이터들을 주고 받는다
개발자가 직접 코드 레벨에서 HTTP 헤더를 컨트롤 해야될 경우 사용하는 대표적인 HTTP 헤더이다
Authorization 헤더 - 클라이언트가 적절한 자격 증명을 가지고 있는지를 확인하기 위한 정보이다 - REST API 기반 애플리케이션의 경우 클라이언트와 서버 간의 로그인(사용자 ID/비밀번호) 인증(Authenticatioin)에 통과한 클라이언트들은 ‘Authorization’ 헤더 정보를 기준으로 인증에 통과한 클라이언트가 맞는지 확인하는 인가 절차를 거친다
User-Agent 헤더 - 애플리케이션을 구현 과정에서 여러가지 유형의 클라이언트가 하나의 서버 애플리케이션에 요청을 전송하는 경우가 많다 - 사용자에 따라 데스크탑이나 노트북의 웹 브라우저를 사용하거나 스마트폰, 태블릿 등 모바일에서 서버에 요청을 보낸다 - 보내오는 요청의 클라이언트를 구분해서 응답 데이터를 다르게 보내줘야 되는 경우가 있다 - 모바일, 데스크탑, 노트북의 화면 크기에 따라 더 많은 정보를 보여주기 위해 각각 데이터의 종류와 크기가 다를 수 있다 - User-Agent 헤더 정보를 이용해서 클라이언트의 요청 브라우져를 구분해서 처리할 수 있다
3. 샘플 어플리케이션에 HTTP Header 적용
1) @RequestHeader 로 개별 헤더 정보 받기
CoffeeController에 적용하여 실행한다
package com.dreamfactory.exam_controller.coffee;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Member;
import java.sql.SQLOutput;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value="v1/coffees"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다
public class CoffeeController {
@PostMapping
//postCoffee() 메서드는 커피 정보를 등록해 준다
public /*String*/ResponseEntity postCoffee(//@RequestParam("coffee")String coffee,
@RequestHeader("user-agent")String userAgent,
@RequestParam("coffeeId")String coffeeId,
@RequestParam("korName")String korName,
@RequestParam("engName")String engName,
@RequestParam("price")int price) {
// System.out.println("# coffee:" + coffee);
// System.out.println("# coffeeId:" + coffeeId);
// System.out.println("# korName:" + korName);
// System.out.println("# engName:" + engName);
// System.out.println("# price:" + price);
//@RequestHeader 로 개별 정보 받아오기
System.out.println("user-agent: " + userAgent);
return new ResponseEntity<>(new Coffee(korName, engName, price),
HttpStatus.CREATED);
//Map객체로 변경
// Map<String, String> map = new HashMap<>();
// map.put("coffee", coffee);
// map.put("coffeeId", coffeeId);
// map.put("korName", korName);
// map.put("engName", engName);
// map.put("price", String.valueOf(price));
//
// //return 값을 ResponseEntity로 변경
// return new ResponseEntity<>(map, HttpStatus.CREATED);
// String reponse =
// "{\"" +
// "coffee\":\""+coffee+"\"," +
// "\"coffeeId\":\""+coffeeId+"\"," +
// "\"korName\":\""+korName+"\"," +
// "\"engName\":\""+engName+"\"," +
// "\"price\":\""+price+
// "\"}";
// return reponse;
}
@GetMapping("/{coffee-id}")
//getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId) {
System.out.println("# coffeeId: " + coffeeId);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다
//getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getCoffees() {
System.out.println("# get Coffees");
return new ResponseEntity<>(HttpStatus.OK);
}
}
실행하면 error가 발생한다
new Coffee에 대한 신규 class를 생성하여 문제를 해결한다
package com.dreamfactory.exam_controller.coffee;
public class Coffee {
public Coffee(String korName, String engName, int price) {
}
}
2) @RequestHeader 로 전체 헤더 정보 받기
MemberController에 적용하여 실행한다
package com.dreamfactory.exam_controller.member;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLOutput;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
public class MemberController {
@PostMapping
public /*String*/ResponseEntity postMember(@RequestHeader Map<String, String> headers,
@RequestParam("email")String email,
@RequestParam("name")String name,
@RequestParam("phone")String phone) {
// System.out.println("# email: " + email);
// System.out.println("# name: " + name);
// System.out.println("# phone: " + phone);
// JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
// Map<String, String> map = new HashMap<>();
// map.put("email", email);
// map.put("name", name);
// map.put("phone", phone);
//@RequestHeader Map을 사용하여 전체 헤더 정보를 받아 온다
for (Map.Entry<String, String> entry : headers.entrySet()){
System.out.println("key: " + entry.getKey() +
", value: " + entry.getValue());
}
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
// return new ResponseEntity<>(map, HttpStatus.CREATED);
//@RequestHeader Map 을 사용함으로 리턴 값의 코드가 변경된다
return new ResponseEntity<>(new Member(email, name, phone), HttpStatus.CREATED);
// String reponse =
// "{\"" +
// "email\":\""+email+"\"," +
// "\"name\":\""+name+"\",\"" +
// "phone\":\"" + phone+
// "\"}";
// return reponse;
}
@GetMapping("/{member-id}")
public /*String*/ResponseEntity getMember(@PathVariable("member-id")long memberId) {
System.out.println("# memberId: " + memberId);
//리턴 값을 변경된 ResponseEntity로 대체
//HttpStatus.OK 는 200, OK를 의미한다
return new ResponseEntity<>(HttpStatus.OK);
//not implementation
// return null;
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다
//getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getMembers() {
System.out.println("# get Members");
//리턴 값을 변경된 ResponseEntity로 대체
//HttpStatus.OK 는 200, OK를 의미한다
return new ResponseEntity<>(HttpStatus.OK);
//not implementation
// return null;
}
}
위의 코드를 실행하면 error가 발생한다
Member class를 생성하라는 메시지가 확인된다
new Member에 대한 신규 class를 생성해 주면 해결된다
package com.dreamfactory.exam_controller.member;
public class Member {
public Member(String email, String name, String phone) {
}
}
postman으로 get/post를 실행하면 아래와 같은 CLI 메시지가 출력된다
3) HttpServletRequest 객체로 헤더 정보 얻기
HttpServletRequest 객체를 이용하면 Request 헤더 정보에 다양한 방법으로 접근이 가능하다
HttpServletRequest는 다양한 API를 지원하지만 특정 헤더 정보에 접근하고자 한다면 @RequestHeader 가 더 용이하다
orderController에 HttpservletRequest 적용하기
package com.dreamfactory.exam_controller.order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value="v1/orders"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)
public class OrderController {
@PostMapping
//postOrder() 메서드는 커피 주문 정보를 등록한다
public /*String*/ResponseEntity postOrder(HttpServletRequest httpServletRequest,
@RequestParam("memberId") String memberId,
@RequestParam("coffeeId") String coffeeId) {
//// Map<String, String> map = new HashMap<>();
//// map.put("memberId", memberId);
//// map.put("coffeeId", coffeeId);
//
// return new ResponseEntity(map, HttpStatus.CREATED);
System.out.println("user-agent: " + httpServletRequest.getHeader("user-agent"));
return new ResponseEntity<>(new Order(memberId, coffeeId),
HttpStatus.CREATED);
// System.out.println("# memberId:" + memberId);
// System.out.println("# coffeeId:" + coffeeId);
//
// String reponse =
// "{\"" +
// "memberId\":\"" + memberId + "\"," +
// "\"coffeeId\":\"" + coffeeId +
// "\"}";
// return reponse;
}
@GetMapping("/{order-id}")
public /*String*/ResponseEntity getOrder(@PathVariable("order-id") long orderId) {
System.out.println("# orderId: " + orderId);
return new ResponseEntity(HttpStatus.OK);
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/orders”)에 매핑된다
//getOrders() 메서드는 주문 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getOrders() {
System.out.println("# get Orders");
return new ResponseEntity(HttpStatus.OK);
}
}
실행하면 error가 발생한다
new Order에 대한 신규 class를 생성하면 해결된다
package com.dreamfactory.exam_controller.order;
public class Order {
public Order (String memberId, String CoffeeId){
};
}
ResponseEntity를 사용한다 - ResponseEntity는 HttpEntity의 확장 클래스이다 - HttpStatus 상태 코드를 추가한 전체 HTTP응답을 표현한다(상태코드, 헤더, 본문) - @Controller @RestController 애너테이션이 붙은 Controller 클래스의 핸들러 메서의 요청에 대한 응답을 표현한다 - RestTemplate에서 외부 API통신에 대한 응답을 전달받아서 표현할 경우에 사용한다
//JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
return new ResponseEntity<>(map, HttpStatus.CREATED)
package com.dreamfactory.exam_controller.member;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLOutput;
//@애터테이션에 의해 자동으로 import 된다
import java.util.HashMap;
import java.util.Map;
@RestController
//Spring MVC에서는 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API의 리소스(자원, Resource)를
처리하기 위한 API 엔드포인트로 동작함을 정의한다
//@RestController가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록 해 준다
//REST API란 REST 방식을 통해서 리소스에 접근하기 위한 서비스 API를 지칭한다
//REST(Representational State Transfer)는 HTTP 네트워크 상의 리소스(Resource, 자원)를 정의하고
해당 리소스를 URI라는 고유의 주소로 접근하는 접근 방식을 의미한다
@RequestMapping("v1/members"/*,produce=MediaType.APPLICATION_JSON_VALUE*/)
//produces애트리뷰트(Attribute)는 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정한다
public class MemberController {
@PostMapping
//postMember() 메서드는 회원 정보를 등록해주는 핸들러 메서드이다
//클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션이다
//클라이언트에서 요청 전송 시 HTTP Method 타입을 동일하게 맞춰 주어야 한다(POST)
public /*String*/ResponseEntity postMember(@RequestParam("email")String email,
@RequestParam("name")String name,
@RequestParam("phone")String phone) {
//@RequestParam() 핸들러 메서드의 파라미터 종류 중 하나이다
/*클라이언트에서 전송하는 요청 데이터를 쿼리 파라미터(Query Parmeter 또는 Query String),
폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송하면 서버 쪽에서 전달 받을 때
사용하는 애너테이션이다*/
// System.out.println("# email: " + email);
// System.out.println("# name: " + name);
// System.out.println("# phone: " + phone);
// JSON문자열 응답 타입을 수작업 코드에서 Map객체로 변경->produce애트리뷰트를 삭제할 수 있다
//MAp 객체를 리턴하면 내부적으로 응답 데이터를 JSON데이터로 자동 변환해야 한다고 인식한다
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
//리턴 값을 변경된 ResponseEntity로 대체
//ResponseEntity 객체를 생성하고 생성자 파라미터로 map과 HttpStatus.CREATED를 반환한다
//HttpStatus.CREATED 는 201, created를 의미한다
return new ResponseEntity<>(map, HttpStatus.CREATED);
//클라이언트 쪽에서 JSON 형식의 데이터를 전송 받아야 하기 때문에
//응답 문자열을 JSON 형식에 맞게 작성한다
// String reponse =
// "{\"" +
// "email\":\""+email+"\"," +
// "\"name\":\""+name+"\",\"" +
// "phone\":\"" + phone+
// "\"}";
// return reponse;
}
@GetMapping("/{member-id}")
//@GetMapping은 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션이다
//@GetMapping 애너테이션의 괄호 안에는 몇 가지 애트리뷰트(Attribute)를 사용할 수 있다
//여기서는 전체 HTTP URI의 일부를 지정했다
//클라이언트에서 getMember() 핸들러 메서드에 요청을 보낼 경우, 최종 URI는 형태는 아래와 같다
// /v1/members/{member-id}
//{member-id}는 회원 식별자를 의미한다
//클라이언트가 요청을 보낼 때 URI로 어떤 값을 지정하느냐에 따라서 동적으로 바뀌는 값이다
public /*String*/ResponseEntity getMember(@PathVariable("member-id")long memberId) {
//getMember() 메서드는 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드이다
//@PathVariable역시 핸들러 메서드의 파라미터 종류 중 하나이다
//@PathVariable의 괄호 안에 입력한 문자열 값은 @GetMapping("/{member-id}") 처럼 중괄호({ })
안의 문자열과 동일해야 한다
//여기서는 두 문자열 모두 “member-id” 로 동일하게 지정했다
//두 문자열이 다르면 MissingPathVariableException이 발생한다
System.out.println("# memberId: " + memberId);
//리턴 값을 변경된 ResponseEntity로 대체
//HttpStatus.OK 는 200, OK를 의미한다
return new ResponseEntity<>(HttpStatus.OK);
//not implementation
// return null;
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다
//getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getMembers() {
System.out.println("# get Members");
//리턴 값을 변경된 ResponseEntity로 대체
//HttpStatus.OK 는 200, OK를 의미한다
return new ResponseEntity<>(HttpStatus.OK);
//not implementation
// return null;
}
}
수정된 코드를 실행한 후 postman에서 post로 request한 결과는 아래와 같다
map에 의한 파라미터 값과 HttpStatus.CREATED의 결과값 201 Created가 반환된다
ResponseEntity를 사용하지 않았던 코드의 결과값 200 OK 가 변경된 결과이다
CoffeeController 와 OrderController 도 개선된 코드로 수정한다
CoffeeController
package com.dreamfactory.exam_controller.coffee;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value="v1/coffees", produces = MediaType.APPLICATION_JSON_VALUE)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다
public class CoffeeController {
@PostMapping
//postCoffee() 메서드는 커피 정보를 등록해 준다
public /*String*/ResponseEntity postCoffee(@RequestParam("coffee")String coffee,
@RequestParam("coffeeId")String coffeeId,
@RequestParam("korName")String korName,
@RequestParam("engName")String engName,
@RequestParam("price")int price) {
// System.out.println("# coffee:" + coffee);
// System.out.println("# coffeeId:" + coffeeId);
// System.out.println("# korName:" + korName);
// System.out.println("# engName:" + engName);
// System.out.println("# price:" + price);
//Map객체로 변경
Map<String, String> map = new HashMap<>();
map.put("coffee", coffee);
map.put("coffeeId", coffeeId);
map.put("korName", korName);
map.put("engName", engName);
map.put("price", String.valueOf(price));
//return 값을 ResponseEntity로 변경
return new ResponseEntity<>(map, HttpStatus.CREATED);
// String reponse =
// "{\"" +
// "coffee\":\""+coffee+"\"," +
// "\"coffeeId\":\""+coffeeId+"\"," +
// "\"korName\":\""+korName+"\"," +
// "\"engName\":\""+engName+"\"," +
// "\"price\":\""+price+
// "\"}";
// return reponse;
}
@GetMapping("/{coffee-id}")
//getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId) {
System.out.println("# coffeeId: " + coffeeId);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다
//getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getCoffees() {
System.out.println("# get Coffees");
return new ResponseEntity<>(HttpStatus.OK);
}
}
OrderController
package com.dreamfactory.exam_controller.order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value="v1/orders"/*, produces = MediaType.APPLICATION_JSON_VALUE*/)
public class OrderController {
@PostMapping
//postOrder() 메서드는 커피 주문 정보를 등록한다
public /*String*/ResponseEntity postOrder(@RequestParam("memberId") String memberId,
@RequestParam("coffeeId") String coffeeId) {
Map<String, String> map = new HashMap<>();
map.put("memberId", memberId);
map.put("coffeeId", coffeeId);
return new ResponseEntity(map, HttpStatus.CREATED);
// System.out.println("# memberId:" + memberId);
// System.out.println("# coffeeId:" + coffeeId);
//
// String reponse =
// "{\"" +
// "memberId\":\"" + memberId + "\"," +
// "\"coffeeId\":\"" + coffeeId +
// "\"}";
// return reponse;
}
@GetMapping("/{order-id}")
public /*String*/ResponseEntity getOrder(@PathVariable("order-id") long orderId) {
System.out.println("# orderId: " + orderId);
return new ResponseEntity(HttpStatus.OK);
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/orders”)에 매핑된다
//getOrders() 메서드는 주문 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public /*String*/ResponseEntity getOrders() {
System.out.println("# get Orders");
return new ResponseEntity(HttpStatus.OK);
}
}
기능 기반 패키지 구조(package-by-feature) - 애플리케이션에서 구현해야 하는 기능을 기준으로 패키지를 구성하는 것을 말한다 - 패키지를 커피 / 회원 / 주문 등 기능으로 구분할 수 있다
계층 기반 패키지 구조(package-by-layer) - 패키지를 하나의 계층(Layer)으로 보고 클래스들을 계층별로 묶어서 관리하는 구조를 말한다 - controller / model / repository / service 등 계층으로 구분할 수 있다
테스트와 리팩토링이 용이하고, 향후에 마이크로 서비스 시스템으로의 분리가 상대적으로 용이한 기능 기반 패키지 구조 사용을 권장한다
4) 애플리케이션의 기능 요구사항 및 클래스 정리
REST API 기반의 애플리케이션에서는 일반적으로 애플리케이션이 제공해야 될 기능을 리소스(Resource, 자원)로 분류한다
리소스에 해당하는 Controller 클래스를 작성한다
5) 엔트리포인트(Entrypoint) 클래스 작성
Spring Boot 기반의 애플리케이션이 정상적으로 실행되기 위해서 가장 먼저 해야될 일은 main() 메서드가 포함된 애플리케이션의 엔트리포인트(Entrypoint, 애플리케이션 시작점)를 작성하는 것이다
부트스트랩(Bootstrap)은 애플리케이션이 실행되기 전에 여러가지 설정 작업을 수행하여 실행 가능한 애플리케이션으로 만드는 단계를 의미한다
엔트리포인트는 spring initializr를 사용하여 프로젝트를 생성하면 자동으로 만들어 진다
exam_controller > src > main > java > com.dreamfactory.exam_controller > ExamControllerApplication 으로 자동 생성된다
//엔트리포인트 클래스
package com.dreamfactory.exam_controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
//자동구성을 활성화 해 준다
//애플리케이션 패키지 내에서 @Component가 붙은 클래스를 검색한 후(scan), Spring Bean으로 등록하는 기능을
활성화 해 준다
//@Configuration 이 붙은 클래스를 자동으로 찾아주고, 추가적으로 Spring Bean을 등록하는 기능을 활성화 해 준다
public class ExamControllerApplication {
public static void main(String[] args) {
SpringApplication.run(ExamControllerApplication.class, args);
}
//SpringApplication.run : Spring 애플리케이션을 부트스트랩하고, 실행하는 역할을 한다
}
6) 애플리케이션의 Controller 구조 작성
회원관리를 위한 MemberController 구조 작성
package com.dreamfactory.exam_controller.member;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//@애터테이션에 의해 자동으로 import 된다
@RestController
//Spring MVC에서는 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API의 리소스(자원, Resource)를
처리하기 위한 API 엔드포인트로 동작함을 정의한다
//@RestController가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록 해 준다
@RequestMapping
//@RequestMapping 은 클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해주는
역할을 한다
//Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL)을 설정할 수 있다
public class MemberController {
}
커피정보를 위한 CoffeeController 구조 작성
package com.dreamfactory.exam_controller.coffee;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/coffees")
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치 URI를 지정한다
//Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL)을 설정할 수 있다.
public class CoffeeController {
}
주문관리를 위한 OrderController 구조 작성
package com.dreamfactory.exam_controller.order;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
public class OrderController {
}
[참조 자료]
※ REST API란?
REST(Representational State Transfer)는 HTTP 네트워크 상의 리소스(Resource, 자원)를 정의하고 해당 리소스를 URI라는 고유의 주소로 접근하는 접근 방식을 의미한다
REST API란 REST 방식을 통해서 리소스에 접근하기 위한 서비스 API를 지칭한다
※REST에서 의미하는 리소스
REST에서 의미하는 자원은 데이터베이스에 저장된 데이터, 문서, 이미지, 동영상 등 HTTP 통신을 통해 주고 받을 수 있는 모든 것을 의미한다
URI는 네트워크 상에 있는 특정 리소스를 식별하는 통합 자원 식별자(Uniform Resource Identifier)를 의미한다
URL은 인터넷에 있는 리소스를 나타내는 통합 리소스 식별자를 의미한다
일상적인 웹 상의 주소는 URL을 의미한다
URI는 URL의 상위 개념으로 볼 수 있다
URI는 리소스를 식별하는 식별자 역할을 하고, URL은 리소스의 위치를 가리킨다 - http://www.itivllage.tistory.com/manage? id = 1 에서 - http://www.itivllage.tistory.com/manage'까지는 리소스의 위치를 가리키는 URL이고 - http://www.itivllage.tistory.com/manage? id = 1는 리소스를 식별하기 위한 'id = 1'이라는 고유 식별자가 붙었으므로 URI이다
※ HTTP 에서 REST API 서비스를 만드는 경우의 REST API URI 작성 규칙
URI의 마지막이 '/' 로 끝나지 않게 한다 - http://www.dreamfactory.com/coffees (good) - http://www.dreamfactory.com/coffees/ (bad)
동사 보다는 명사를 사용한다 - http://www.dreamfactory.com/coffees (good) -http://www.dreamfactory.com/coffees/update(bad)
단수형 보다는 복수형 명사를 사용한다 - http://www.dreamfactory.com/coffees (good) -http://www.dreamfactory.com/coffee (bad)
MemberController에 클라이언트의 요청을 처리할 핸들러 메서드(Handler Method)가 아직 없기 때문에 localhost를 요청해도 에러가 발생한다
작업 중인 애플리케이션은 REST API 기반 애플리케이션이기 때문에 응답 메시지는 JSON 형식으로 클라이언트에게 전달된다
1) MemberController의 핸들러 메서드(Handler Method) 작성
회원 이메일 주소: email
회원 이름: name
회원 전화번호: phoneNumber
위의 정보를 기준으로 작성한다
package com.dreamfactory.exam_controller.member;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLOutput;
//@애터테이션에 의해 자동으로 import 된다
@RestController
@RequestMapping(value ="v1/members", produces = MediaType.APPLICATION_JSON_VALUE)
//produces애트리뷰트(Attribute)는 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정한다
//JSON 형식의 데이터를 응답 데이터로 전송하기 위해 MediaType.APPLICATION_JSON_VALUE 값을 설정했다
//이 값을 설정하지 않으면 현재 코드에서는 JSON 형식의 데이터를 응답으로 전송하지 않고, 문자열 자체를 전송한다
public class MemberController {
@PostMapping
//postMember() 메서드는 회원 정보를 등록해주는 핸들러 메서드이다
//클라이언트의 요청 데이터(request body)를 서버에 생성할 때 사용하는 애너테이션이다
//클라이언트에서 요청 전송 시 HTTP Method 타입을 동일하게 맞춰 주어야 한다(POST)
public String postMember(@RequestParam("email")String email,
@RequestParam("name")String name,
@RequestParam("phone")String phone) {
//@RequestParam() 핸들러 메서드의 파라미터 종류 중 하나이다
/*클라이언트에서 전송하는 요청 데이터를 쿼리 파라미터(Query Parmeter 또는 Query String),
폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송하면 서버 쪽에서 전달 받을 때
사용하는 애너테이션이다*/
System.out.println("# email: " + email);
System.out.println("# name: " + name);
System.out.println("# phone: " + phone);
//클라이언트 쪽에서 JSON 형식의 데이터를 전송 받아야 하기 때문에
//응답 문자열을 JSON 형식에 맞게 작성한다
String reponse =
"{\"" +
"email\":\""+email+"\"," +
"\"name\":\""+name+"\",\"" +
"phone\":\"" + phone+
"\"}";
return reponse;
}
@GetMapping("/{member-id}")
//@GetMapping은 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션이다
//@GetMapping 애너테이션의 괄호 안에는 몇 가지 애트리뷰트(Attribute)를 사용할 수 있다
//여기서는 전체 HTTP URI의 일부를 지정했다
//클라이언트에서 getMember() 핸들러 메서드에 요청을 보낼 경우, 최종 URI는 형태는 아래와 같다
// /v1/members/{member-id}
//{member-id}는 회원 식별자를 의미한다
//클라이언트가 요청을 보낼 때 URI로 어떤 값을 지정하느냐에 따라서 동적으로 바뀌는 값이다
public String getMember(@PathVariable("member-id")long memberId) {
//getMember() 메서드는 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드이다
//@PathVariable역시 핸들러 메서드의 파라미터 종류 중 하나이다
//@PathVariable의 괄호 안에 입력한 문자열 값은 @GetMapping("/{member-id}") 처럼 중괄호({ }) 안의 문자열과 동일해야 한다
//여기서는 두 문자열 모두 “member-id” 로 동일하게 지정했다
//두 문자열이 다르면 MissingPathVariableException이 발생한다
System.out.println("# memberId: " + memberId);
//not implementation
return null;
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/members”)에 매핑된다
//getMembers() 메서드는 회원 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public String getMembers() {
System.out.println("# get Members");
//not implementation
return null;
}
}
RestController - Spring MVC에서 웹 요청을 하기 위한 준비가 완료되었음을 의미한다
RequestMapping - RequestMapping(value="파일경로", method={HTTP 요청값}) 의 형식으로 작성한다 - HTTP 값을 지정하지 않으면 모든 HTTP요청에 매핑된다 - Request에 대한 헤더 값을 지정하면 매핑 범위를 좁힐 수 있다
헤더 값을 지정하여 범위를 좁힌 예)
@RequestMapping(value = "/ex/dream", headers = "key=val", method = GET)
@ResponseBody
public String getDreamWithHeader() {
return "Get some Dream with Header";
}
HTTP POST에 요청된 예)
@RequestMapping(value = "/ex/dream", method = POST)
@ResponseBody
public String postDream() {
return "Post some Dream";
}
headers속성을 사용하여 헤더를 여러개 지정할 수 있다
@RequestMapping(
value = "/ex/dream",
headers = { "key1=val1", "key2=val2" }, method = GET)
@ResponseBody
public String getDreamWithHeaders() {
return "Get some Dream with Header";
}
※ MemberController : postMember() 요청 및 응답
HTTP POST Method와 요청 URI를 입력한다
[Body] 탭에서 ‘x-www-form-urlencoded’ 형식의 요청 데이터를 KEY/VALUE 형태로 입력한다
JSON 형식의 응답 데이터를 전달 받을 수 있다
우측 중간을 보면 ‘200’, ‘OK’ 라는 값을 통해서 클라이언트가 정상적으로 응답을 전달 받았음을 알 수 있다
2) CoffeeController의 핸들러 메서드(Handler Method) 작성
커피 식별자: coffee
식별자: coffeeId
커피명(영문): engName
커피명(한글): korName
가격: price
package com.dreamfactory.exam_controller.coffee;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value="v1/coffees", produces = MediaType.APPLICATION_JSON_VALUE)
//v1은 버젼을 의미한다. coffees는 데이터 post, get 의 조회 위치를 의미한다
public class CoffeeController {
@PostMapping
//postCoffee() 메서드는 커피 정보를 등록해 준다
public String postCoffee(@RequestParam("coffee")String coffee,
@RequestParam("coffeeId")String coffeeId,
@RequestParam("korName")String korName,
@RequestParam("engName")String engName,
@RequestParam("price")int price) {
System.out.println("# coffee:" + coffee);
System.out.println("# coffeeId:" + coffeeId);
System.out.println("# korName:" + korName);
System.out.println("# engName:" + engName);
System.out.println("# price:" + price);
String reponse =
"{\"" +
"coffee\":\""+coffee+"\"," +
"\"coffeeId\":\""+coffeeId+"\"," +
"\"korName\":\""+korName+"\"," +
"\"engName\":\""+engName+"\"," +
"\"price\":\""+price+
"\"}";
return reponse;
}
@GetMapping("/{coffee-id}")
//getCoffee() 메서드는 커피 정보을 클라이언트에게 제공하는 핸들러 메서드이다
public String getCoffee(@PathVariable("coffee-id")long coffeeId) {
System.out.println("# coffeeId: " + coffeeId);
return null;
}
@GetMapping //별도의 URI를 지정해주지 않았기 때문에 클래스 레벨의 URI(“/v1/coffees”)에 매핑된다
//getCoffees() 메서드는 커피 목록을 클라이언트에게 제공하는 핸들러 메서드이다
public String getCoffees() {
System.out.println("# get Coffees");
return null;
}
}
소프트웨어 설계에서 세 가지 구성 요소인 모델(Model), 뷰(View), 컨트롤러(Controller)를 이용한 설계 방식을 말한다
웹 계층을 담당하는 몇가지 모듈 중에서 서블릿(Servlet) API를 기반으로 클라이언트의 요청을 처리하는 모듈을 spring-webmvc라고 한다
Spring Web MVC를 줄여서 'Spring MVC' 라고 한다
Spring MVC가 웹 프레임워크의 한 종류이기 때문에 'Spring MVC 프레임워크'라고도 한다
클라이언트의 요청을 편리하게 처리해주는 프레임워크이다.
※ 서블릿(Servlet)
클라이언트의 요청을 처리하도록 특정 규약에 맞추어서 Java 코드로 작성하는 클래스 파일이다
아파치 톰캣(Apache Tomcat)은 서블릿들이 웹 애플리케이션으로 실행이 되도록 해주는 서블릿 컨테이너(Servlet Container) 중 하나이다
Spring MVC 내부에서는 서블릿을 기반으로 웹 애플리케이션이 동작한다
2) 모델(MODEL)
Spring MVC 기반의 웹 애플리케이션은 클라이언트의 요청을 전달 받으면 요청 사항을 처리하기 위한 작업을 한다
작업 완료 후 클라이언트에게 응답으로 돌려주는 처리한 작업의 결과 데이터를 Model이라고 한다
클라이언트의 요청 사항을 구체적으로 처리하는 영역을 서비스계층(Service Layer)이라고 한다
요청 사항을 처리하기 위해 Java 코드로 구현한 것을 비즈니스 로직(Business Logic)이라고 한다
3) 뷰(View)
Model 데이터를 이용해서 웹브라우저 같은 클라이언트 애플리케이션의 화면에 보여지는 리소스(Resource)를 제공하는 역할을 한다
View의 형태별 종류 ▶ HTML 페이지의 출력 - 클라이언트 애플리케이션에 보여지는 HTML 페이지를 직접 렌더링해서 클라이언트 측에 전송하는 방식이다 - 기본적인 HTML 태그로 구성된 페이지에 Model 데이터를 채워서 최종 HTML 페이지를 만든 후 클라이언트 측에 전송한다 - Spring MVC에서 지원하는 HTML 페이지 출력 기술에는 Thymeleaf, FreeMarker, JSP + JSTL, Tiles 등이 있다
▶PDF, Excel 등의 문서 형태로 출력 - Model 데이터를 가공해서 PDF 문서나 Excel 문서를 만들어서 클라이언트 측에 전송하는 방식이다 - 문서 내에서 데이터가 동적으로 변경되어야 하는 경우 사용할 수 있다
▶ XML, JSON 등 특정 형식의 포맷으로의 변환 - Model 데이터를 특정 프로토콜 형태로 변환해서 클라이언트 측에 전송하는 방식이다 - 특정 형식의 데이터만 전송하고, 프런트엔드 측에서 이 데이터를 기반으로 HTML 페이지를 만드는 방식이다 - 프런트엔드 영역과 백엔드 영역이 명확하게 구분되므로 개발 및 유지보수가 상대적으로 용이하다 - 프런트엔드 측에서 비동기 클라이언트 애플리케이션을 만드는 것이 가능해진다
※ JSON(JavaScript Object Notation)
JSON은 Spring MVC에서 클라이언트 애플리케이션과 서버 애플리케이션이 주고 받는 데이터 형식이다
과거에는 XML 형식의 데이터가 많이 사용되었다
현재는 XML보다 상대적으로 가볍고, 간결한 JSON 형식을 사용하고 있는 추세이다
JSON은 {"속성", "값"} 의 형태로 작성된다
4) 컨트롤러(Controller)
클라이언트 측의 요청을 직접적으로 전달 받는 엔드포인트(Endpoint)이다
Model 과 View 중간에서 상호 작용을 해주는 역할을 한다
클라이언트 측의 요청을 전달 받아서 비즈니스 로직을 거친 후에 Model 데이터가 만들어지면 View로 전달하는 역할을 한다
2. Spring MVC의 동작 방식
Client가 요청 데이터 전송 → Controller가 요청 데이터 수신 → 비즈니스 로직 처리 → Model 데이터 생성
→ Controller에게 Model 데이터 전달 → Controller가 View에게 Model 데이터 전달 → View가 응답 데이터 생성
DispatcherServlet이 역활이 많아 보이지만 요청에 대한 처리는 다른 구성 요소들에게 위임(Delegate)하고 있다
DispatcherServlet이 애플리케이션의 가장 앞단에 배치되어 다른 구성요소들과 상호작용하면서 클라이언트의 요청을 처리하는 패턴을 Front Controller Pattern이라고 한다
public class Question6_letterCapitalize {
public static void main(String[] args) throws Exception{
//문자열을 입력받을 객체를 생성
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//문자열을 출력할 객체를 생성
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
//문자열을 입력받을 변수를 생성
String words = br.readLine();
String arr[] = words.split(" ");
//조건 생성
if(arr.length == 0 || words.isBlank()) { //빈 문자열이거나 공백이면
bw.write(words); //그대로 출력한다
} else {
Pattern pattern = Pattern.compile("([a-z])([a-z]*)"); //모든 문자열에 대한 정규식
Matcher matcher = pattern.matcher(words); //pattern과 일치하는 문자열을 찾아서 객체에 전달
//위의 두 코드를 하나로 합칠 수 있다
// Matcher match = Pattern.compile("([a-z])([a-z]*)", Pattern.CASE_INSENSITIVE).matcher(words);
StringBuffer str = new StringBuffer(); //변경한 문자열을 저장할 공간을 생성한다
while (matcher.find()) { //matcher.find()) 메서드로 매칭되는 위치로 이동한다
//appendReplacement 메서드로 (str, 바꿀 문자열)을 통해 바뀐 문자열을 str에 저장한다
matcher.appendReplacement(str, matcher.group(1).toUpperCase() + matcher.group(2).toLowerCase());
}
matcher.appendTail(str); //더이상 찾을 문자열이 없으므로 꼬리를 붙이기 위한 메서드를 사용한다
bw.write(str.toString()); //str 값을 String으로 변환하여 출룍한다
}
// bw.flush();//남아있는 데이터를 모두 출력
bw.close();//스트림 닫음
}
}
Result
> Task :classes
java is good
> Task :Question6_letterCapitalize.main()
Java Is Good
public class String_getTextOfLocation {
//charAt() 사용하여 문자 출력
public static Character getFirstCharacter(String str){
if(str == null || str.length() == 0) { //문자열이 null이거나 문자열 길이가 0이면
return null; //null을 반환
} else {
return (Character) str.charAt(0); //그렇지 않으면 문자열의 0번째 문자를 반환
}
}
public static void main(String[] args)
{
String str = "Hello welcome to develop world!!";
System.out.println("문자열 : " + str);
System.out.print("문자열에 첫번째 문자는 : " + getFirstCharacter(str));
}
}
문자열 : Hello welcome to develop world!!
문자열에 첫번째 문자는 : H
2. substring( )
startIdx에서 시작하는 하위 문자열부터 나머지 모든 문자를 반환한다
startIdx, endIdx를 전달하면 startIdx에서 시작하여 endIdx-1번째 문자까지 반환한다
public String substring(int startIdx)
public String substring(int startIdx, int endIdx)
public class String_getTextOfLocation_substring {
public static void main(String[] args){
String str = "Hello welcome to develop world!!";
//01234567890123456789012345678901
System.out.println("문자열 : " + str);
System.out.println("시작점 index(3): " + str.substring(3)); //4번째 문자부터 전체 반환
System.out.println("index(2)부터 index(6)까지의 문자 : " + str.substring(2, 7)); //3번째 문자부터 6번째 앞 문자까지
System.out.println("index(3)부터 index(7)까지의 문자 : " + str.substring(3, 8)); //3번째 문자부터 8번째 앞 문자까지
System.out.print("문자열에 첫번째 문자는 : " + getFirstCharacter(str)); //첫번째 문자열 가져오기
}
public static String getFirstCharacter(String str)
{
if(str == null || str.length() == 0) //빈 문자열일 경우의 조건
return null;
else
return str.substring(0, 1);
}
}
문자열 : Hello welcome to develop world!!
시작점 index(3): lo welcome to develop world!!
index(2)부터 index(6)까지의 문자 : llo w
index(3)부터 index(7)까지의 문자 : lo we
문자열에 첫번째 문자는 : H
3. toCharArray( )
index 값을 사용하여 배열의 요소에 액세스할 수 있다
문자열을 char 데이터 유형의 배열로 변환할 수 있다면 index를 사용하여 모든 요소를 가져올 수 있다
인덱스 0을 통해 첫 번째 문자를 가져올 수 있다
toCharArray() 메서드를 사용하여 문자열을 문자 배열로 변환한다
public class String_getTextOfLocation_toCharArray {
public static Character getFirstCharacter(String str)
{
if(str == null || str.length() == 0) //빈 문자열일 경우 null 반환
return null;
else
{
char[] charArr = str.toCharArray(); //문자열을 배열로 변환
return charArr[0];
}
}
public static void main(String[] args)
{
String str = "Hello welcome to develop world!!";
System.out.println("문자열 : " + str);
System.out.print("문자열에 첫번째 문자는 : " + getFirstCharacter(str));
}
}
문자열 : Hello welcome to develop world!!
문자열에 첫번째 문자는 : H
public class Question3_powerOfTwo {
public static void main(String[] args) {
//수를 입력받는다
Scanner sc = new Scanner(System.in);
System.out.println("수를 입력하세요 : ");
long num = sc.nextInt();
//2의 거듭제곱인지 확인 후 boolean 값을 출력한다
while (num >= 1) {
if (num % 2 == 0 || num == 1) {
System.out.println(true);
} else {
System.out.println(false);
}
break;
}
}
}
배열을 입력받아 차례대로 배열의 첫 요소와 마지막 요소를 키와 값으로 하는 HashMap을 리턴
Code
public class Question1_transformFirstAndLast {
public static void main(String[] args) {
//입력받을 데이터의 개수를 입력받는다
Scanner sc = new Scanner(System.in);
System.out.println("입력받을 문자의 개수는?");
Integer count = sc.nextInt();
sc.nextLine();
//배열에 들어갈 문자열을 입력한다
System.out.println(count + "개의 문자를 입력하세요");
String[] arr = new String[count];
for (int i = 0; i < count; ++i) {//
arr[i] = sc.nextLine();
//배열에 빈문자열이 입력되면 null을 반환한다
if(arr[i].length()==0){
arr[i]=null;
}
}
//저장된 배열의 데이터 중에서 첫번째와 마지막 데이터를 key와 value 로 저장한다
HashMap result = new HashMap();
//배열의 첫번째와 마지막 값을 key와 value로 저장한다
result.put(arr[0], arr[arr.length - 1]);
//저장된 값을 출력한다
System.out.println("{" + result.keySet() + " : " + result.values() + "}");
}
}
Result
입력받을 문자의 개수는?
5
5개의 문자를 입력하세요
a
s
d
f
g
{[a] : [g]}
HashMap은 key와 value 값으로 구성된다 - HashMap(key, value) - key 값은 중복되면 않된다 - value 값은 중복될 수 있다
HashMap = Hash Table 에 저장 = 배열(Array) + 링크드리스트(LinkedList) 로 구성된다
HashMap은 순서를 유지하지 않아도 된다 - 순서를 유지하고 싶다면 LikedHashMap을 사용하면 된다
HashMap은 Hashing 기법으로 만들어진다
HashMap map = new HasgMap();
map.put("key", "value"); //저장하기 위한 명령어로 put를 사용한다
key와 value 값 한쌍을 Entry라고 한다
2. Hashing
hash function은 key값을 받아서 해당되는 index 값(저장위치)을 반환하는 함수이다
반환되는 index 값을 hash code라고 한다
hash 함수를 이용하여 hash 테이블에 데이터를 저장하고 반환하는 작업을 hashing이라고 한다 - key값이 같으면 항상 같은 값을 반환한다 - key값이 달라도 같은 값을 반환할 수 있다
hash table은 배열과 링크드 리스트의 조합이다 - 배열의 접근성과 링크드리스트의 변경 용이성을 모두 가지고 있다
3. HashMap 메서드
1) HashMap
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map m)
2) Object Method
Object put(Object key, Object value) - 데이터를 저장할 때 사용한다 - key와 value를 묶어서 저장한다(key=value)
void putAll(Map m) - 지정한 Map m의 값을 모두 저장할 때 사용한다
Object remove(Object key) - key 데이터를 삭제할 때 사용한다
Object replace(Object key, object value) - 지정한 key의 value를 변경할 때 사용한다
boolean replace(Object key, Object oldValue, Object newValue) - 지정한 key에 해당되는 value를 변경할 때 사용한다
3) HashMap에 저장된 데이터를 읽어오는 Method
Set entrySet() - key와 value 쌍으로 구성된 데이터를 호출한다
Set keySet() - key 데이터만 호출한다
Collection value() - value 데이터만 호출한다
4) HashMap에 저장된 데이터를 조회하는 Method
Object get(Object key) - 지정한 key에 해당하는 value를 반환한다
Object getOrDefault(Object key, Object defaultValue) - 주어진 key가 데이터에 없을 경우, 지정한 defaultValue 데이터를 반환한다
boolean containsKey(object key) - 주어진 key가 key 데이터에 있는지 여부를 확인한다 - true / false 로 반환한다
boolean containsValue(Object vaalue) - 주어진 value가 value 데이터에 있는지 여부를 확인한다 - true / false 로 반환한다
5) 정량적 Method
int size() - 데이터의 크기를 확인한다 boolean isEmpty() - 데이터가 비었는지 확인한다
void clear() - 데이터를 삭제한다
Object clone() - 데이터를 복제한다
4. 예제
import java.util.HashMap;
import java.util.Scanner;
public class HashMap_IdPw {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("Id1", "1111");
map.put("Id2", "2222");
map.put("Id3", "3333");
// System.out.println(map);
Scanner scanner = new Scanner(System.in);
while (true) { //반복문을 실행
System.out.println("Id와 Pw를 입력하세요");
System.out.println("Id : ");
String id = scanner.nextLine().trim(); //trim 은 앞 뒤 공백을 없애준다
System.out.println("Pw : ");
String pw = scanner.nextLine().trim();
System.out.println(); //줄바꿈 역활
if (!map.containsKey(id)) { //containsKey는 key 값 전체를 가리킨다
System.out.println("일치하는 id가 없습니다" + "\n" + "다시 입력해 주시기 바랍니다");
continue; //다음 반복 while 문으로 이동
}
if (!map.get(id).equals(pw)) { //get(id)는 key 값 중에 id와 일치하는 값이 있으면 value 값을 반환한다
System.out.println("비밀번호가 일치하지 않습니다" + "\n" + "다시 입력해 주시기 바랍니다");
} else { //id와 pw가 일치하면 문장 출력 후 반복문 털출
System.out.println("id와 비밀번호가 일치합니다");
break;
}
}
}
}
import java.util.*;
public class HashMap_SumAverage {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("kim", 90);
map.put("lee", 50);
map.put("cha", 80);
map.put("park", 100);
map.put("choi", 70);
// System.out.println(map);
//전체 map에 저장된 데이터에서 값을 호출,조회하는 코드
Set set = map.entrySet(); //map에 저장된 데이터를 key,value 쌍으로 호출한다
Iterator iter = set.iterator(); //set에 데이터가 남아 있는지 확인한다
while(iter.hasNext()) {
Map.Entry me = (Map.Entry)iter.next(); //Map 인터페이스 안의 Entry 인터페이스
System.out.println("이름 : " + me.getKey() + ", 점수 : " + me.getValue());
}
//전체 map에서 key 데이터만을 호출하는 코드
set = map.keySet(); //key에 저장된 값만 가져온다
System.out.println("참가자 명단 : " + set);
//전체 map에서 value 데이터만을 호출하는 코드
Collection values = map.values(); //value 데이터를 가져온다
iter = values.iterator();
int total = 0;
while(iter.hasNext()) { //hasNext()는 읽어 올 요소가 남았는지 확인한다
int i = (int)iter.next(); //next() 메소드는 읽어 올 요소가 남았는지 확인한다
total = total +i;
}
System.out.println("총점 : " + total);
System.out.println("평균 : " + (float)total/set.size()); //정수로 결과값이 나오므로 float로 형변환 해 준다
System.out.println("최고점수 : "+ Collections.max(values));
System.out.println("최저점수 : "+ Collections.min(values));
}
}
import java.util.*;
public class HashMap_SumAverage {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("kim", 90);
map.put("lee", 50);
map.put("cha", 80);
map.put("park", 100);
map.put("choi", 70);
// System.out.println(map);
//전체 map에 저장된 데이터에서 값을 호출,조회하는 코드
Set set = map.entrySet(); //map에 저장된 데이터를 key,value 쌍으로 호출한다
Iterator iter = set.iterator(); //set에 데이터가 남아 있는지 확인한다
while(iter.hasNext()) {
Map.Entry me = (Map.Entry)iter.next(); //Map 인터페이스 안의 Entry 인터페이스
System.out.println("이름 : " + me.getKey() + ", 점수 : " + me.getValue());
}
//전체 map에서 key 데이터만을 호출하는 코드
set = map.keySet(); //key에 저장된 값만 가져온다
System.out.println("참가자 명단 : " + set);
//전체 map에서 value 데이터만을 호출하는 코드
Collection values = map.values(); //value 데이터를 가져온다
iter = values.iterator();
int total = 0;
while(iter.hasNext()) { //hasNext()는 읽어 올 요소가 남았는지 확인한다
int i = (int)iter.next(); //next() 메소드는 읽어 올 요소가 남았는지 확인한다
total = total +i;
}
System.out.println("총점 : " + total);
System.out.println("평균 : " + (float)total/set.size()); //float로 형변환 해 준다
System.out.println("최고점수 : "+ Collections.max(values));
System.out.println("최저점수 : "+ Collections.min(values));
}
}
1. 통합개발환경(Integrated Development Environment, IDE) 선택
통합개발환경의 결정은 매우 중요하다
개발자는 어떤 통합개발환경을 선택하는가에 따른 영향을 받을 수 있으므로 팀 단위로 개발 업무를 진행하기 위하여 통합개발환경을 맞추고 개발을 진행하는 것이 좋다
통합개발환경의 선택 요인은 개발자의 툴 학습 정도에 따른 생산성이나 기타 여러 요인들을 종합적으로 검토하여 결정한다
Jetbrains사의 IntelliJ는 상용 프로그램으로 최근 전세계적으로 많은 개발자가 사용하고 있다
이전까지는 무료로 제공되는 Eclipse를 많이 사용했다 - 프로그램이 무겁고 개발에 따른 구현 항목이 많아지면 속도가 느려지는 등의 문제가 많다
IntelliJ는 Eclipse의 여러 문제를 해결하고 더 강력한 사용 경험을 제공한다
IntelliJ는 무료 버젼도 제공한다 - 무료인 커뮤니티 버젼은 학습 수준의 개발을 경험하는데 크게 무리가 없다 - 상용 버전의 전체 기능을 제공하지는 않으며 일부 제한된 기능만을 사용할 수 있다
2. JDK(Java Development Kit) 선택
JDK는 자바 언어로 자바 프로그래밍을 할 수 있게하는 자바 개발 도구이다
JDK를 설치하지 않으면 자바 언어로 개발 할 수 없다
예전에는 JDK가 무료로 제공되었지만 Oracle 기업에서 JDK를 관리하면서 유료 도구로 전환되었다
OpenJDK는 무료 버전의 자바 개발 도구이며 여러 벤더에서 제공하고 있다
<벤더별 OpenJDK 목록 - 출처 : Stackoverflow>
일반적으로 AdoptOpenJDK를 많이 사용한다
3. 프레임워크(Framework) 선택
프레임워크는 Spring-boot를 선택한다
4. 빌드 방식 선택
빌드 방식은 크게 두가지로 Maven 과 Gradle 이 있다
프로젝트를 진행하는 과정에서 구현하는 코드 외에 많은 외부 라이브러리들을 참조해서 개발을 하게 되는데 빌드 툴을 사용하지 않으면 외부 라이브러리들을 모두 직접 관리해야 하고 프로젝트 빌드 시에도 빌드에 대한 표준을 설정하여 빌드 해야한다
빌드 툴을 사용하면 개발 속도를 높이고, 생산성을 증대시킬 수 있다
이전에는 Ant라는 것을 많이 사용했지만 Maven이 Ant를 넘어서 더 많은 개발자들이 사용하고 있다 - Maven은 빌드의 요소를 XML로 정의하고 있는데 XML은 사람의 시각에서 보기 편한지 않다 - 여러 라이브러리들이 의존 관계를 형성하는 경우에도 어려움을 겪을 수 있다
Gradle은 최근에 선호도가 매우 높아졌다 - Gradle은 Groovy라는 것을 사용하기 때문에 동적인 빌드 시에 여러 문제들을 Groovy 스크립트 등을 통해 해결한다 - 처리 속도가 Maven에 비해 월등히 빠르다
5. 형상관리(Software Configuration Management) 방법 결정
Git과 같은 형상관리 방법을 사용하지 않으면 개발자는 개발을 진행함에 있어 여러가지 어려움을 경험한다 - 첫번째는 개발중인 코드가 예상치 못한 사고로 인해 통째로 날아갈 수 있다 - 두번째는 버전 관리가 어렵다 : 작성한 코드를 수정해야 할 경우에 한번에 완벽하게 수정이 완료된다면 문제 없지만, 잘못 수정될 수 있다 : 형상관리 툴은 버져닝(버전관리)을 한다 : 소스 코드를 원격지에 백업하는 용도 외에도 코드의 히스토리를 기록하여 원하는 버전으로 대체할 수 있다 - 세번째는 협업을 하기에 용이하다 : 규모가 큰 프로젝트를 진행할 경우 개발자 혼자서 모든 것을 구현하기는 어렵다 : 여러 사람이 협업하여 진행할 경우에는 소스코드가 중복되거나 같은 부분을 다르게 수정하여 소스 코드간에 ‘충돌' 현상이 발생할 수 있다 : 동일한 소스 코드의 업무별 분리와 취합이 힘들고 장소의 제약을 받을 확률도 매우 높다 : 형상관리를 활용하면 여러 사람이 함께 공통의 코드를 관리하고 취합할 수 있으며, 용도별로 저장소를 구분하여 ‘충돌(Conflic)'과 ‘병합(Merge)'의 문제가 해결된다
형상관리 서비스는 많은 제품이 있다 - Git 외에도 이전에는 SVN이라는 형상관리 방식을 많이 사용했다 - SVN은 아직도 많은 개발자와 기업에서 사용하고 있다 - Git으로의 전환을 늦추는 이유는 비용과 시간을 고려한 업무 효율성 때문이다 - Git은 Github 라는 강력한 서비스를 통해 전 세계에서 가장 많이 사용하는 형상관리 서비스이다
6. IntelliJ 설정
인텔리제이의 Setting > Version Control > Git 에서 Git을 설치한다
인텔리제이의 Setting > Version Control > GitHub 에서 +를 눌러 자신의 Git을 등록한다
신규 프로젝트에서 Alt + ` 을 누른다 - 화면 창에서 commit를 선택하여 저장소를 연결한다
Commit 을 선택하면 IntelliJ의 화면 구성이 변하고 Commit to main이라는 탭이 나타난다
Github Repository에 최초의 소스코드를 등록할 예정이므로 프로젝트 생성시 만들어진 모든 파일들이 업로드 되어야 한다 - Unversioned Files를 체크하여 Commit 대상을 모두 선택한다
아래 ‘Commit Message’ 항목에 커밋 내용을 initial 이라고 적고 ‘Commit and Push’ 버튼을 눌러 Github Repository에 등록한다
Github 계정에는 연결되어 있지만 어떤 Repository에 연결할지는 설정하지 않았다
Commit and push 전 해당 프로젝트가 연결될 Repository를 지정해야 한다 - Define remote를 누르고 Github에서 생성한 Repository의 주소를 적습니다.
Repository가 연결되고 난 뒤 push 버튼을 눌러 원격지에 내보낸다
프로젝트명에서 우클릭하여 나타나는 메뉴 중에서 ignoir를 선택한다 - 아래 화면처럼 설정한 후 generate을 클릭하면 프로젝트 파일에 ingnoir 파일이 생성된다
메인 개발 코드를 그대로 복사하여 기존의 메인 개발 코드를 건드리지 않고 새로운 기능을 개발할 수 있는 버전 관리 기법이다
처음에 Git 리포지토리를 생성하면 나오는 main 브랜치에서만 작업을 하다가 새로운 기능 개발을 위해 feature 브랜치를 새로 생성하는 경우, 기존 main 브랜치에서의 작업은 유지하고 새로운 feature 브랜치에서 자유롭게 코드를 추가 및 삭제할 수 있다
2. Git branch
1) main 브랜치
main 브랜치는 사용자에게 언제든 배포할 수 있으며, 사용자에게 언제든 제품으로 출시할 수 있는 브랜치이다
회사에 따라서 master, prod, production 등으로 사용하기도 한다
일정 기준을 충족했고, 핵심 기능이 완성되었으면 main 브랜치로 배포할 수 있다 - 대표적인 기능이 완성되어 있다 - 기존 기획했던 레이아웃이나 전체적인 디자인이 대부분 완성되어 있다 - 클라이언트, 서버, 데이터베이스가 공개된 웹에서 정상적으로 통신할 수 있다 - 최소한의 보안이 마련되어 있다 : 브라우저에서 개발 버전에서 사용하던 secret이나 유저의 비밀번호가 노출되지 않도록 한다 : 유저의 기밀 정보 조회를 위해 인증 토큰, 세션이 필요하도록 한다
2) dev 브랜치
다음 버전 배포를 위한 개발 중인 브랜치이다
main 브랜치에서부터 브랜치를 생성한다
main 브랜치와 dev 브랜치는 Github 리포지토리에 항상 업데이트 되어있어서 팀원의 코드 리뷰를 받고 진행한다
가능하면 모든 팀원이 확인 가능하도록 코멘트를 Github Pull Request에 남기는 것을 권장한다
3) 보조 브랜치
feature 브랜치라고 한다
feature 브랜치는 기능 개발, 리펙토링, 문서 작업, 단순 오류 수정 등 다양한 작업을 기록하기 위한 브랜치이다
분류를 세세하게 나누는 경우에는 refactor, fix, docs, chore와 같이 세세하게 커밋 메시지나 브랜치명에 prefix를 달기도 한다
아래는 feature 브랜치 이름과 커밋 메시지의 예시이다
hash (브랜치 명) 커밋 메시지
2f85eea (feat/create-todo) feat: Todo 추가 기능
2ad0805 (fix/var-name) fix: 변수 네이밍 컨벤션에 맞게 변수명 변경 (ismale => isMale)
e7ce3ad (refactor) refactor: 불필요한 for 루프 삭제
feature 브랜치는 보통 각 개인의 로컬 리포지토리에서 만들고 작업한다
feature 브랜치는 기능 개발을 위한 브랜치이기 때문에 2명 이상 같이 작업하는 경우가 없다
커밋 기록을 남기는 일반적인 rebase-and-merge 와 커밋 기록을 정리하는 squash-and-merge 등의 merge가 있다
일반적으로 feature 브랜치는 머지하고 나서 삭제하지만, 복원해야 할 필요성이 있는 경우는 남겨두기도 한다
브랜치를 새로 생성하는 경우, -c를 붙인다
git switch -c feature
checkout 이라는 명령어도 사용할 수 있다
git checkout -b feature
feature 브랜치로 이동한다
git switch feature
기존에 있던 main 브랜치로 HEAD를 변경 시에는 -c를 붙이지 않는다
git switch main
git checkout main
4. 브랜치 합치기(git merge)
기능 개발이 끝나면 브랜치를 main 브랜치와 합칠 수 있다
feature 브랜치에서 기능 개발
git commit -m "기능1의 세부 기능1"
git commit -m "기능1의 세부 기능2"
git commit -m "기능1 개발 완료"
머지를 위해 main 브랜치로 전환
git switch main
main 브랜치로 feat/todo 브랜치를 병함
git merge feat/todo
프로젝트 개발 시에는 브랜치를 Github의 pull request 기능을 이용하여 변경 내역을 충분히 확인한다 - 로컬에서 머지하지 않고 feature 브랜치를 push하여 pull request를 요청하는 것이 문제 발생을 감소시킨다
기능 개발이 진행
git commit -m "기능1의 세부 기능1"
git commit -m "기능1의 세부 기능2"
git commit -m "기능1 개발 완료"
Github 리포지토리로 푸시
git push origin feat/todo
Github에서 Pull Request 실행
5. 브랜치 삭제(git branch -d)
머지된 feature 브랜치는 이미 dev 브랜치에 기록이 완벽하게 남아있기 때문에 남겨둘 이유가 없으므로 삭제한다
원격 레포지토리에서 pull request가 성공적으로 마무리되면, 아래 스크린샷처럼 브랜치를 삭제하는 버튼을 눌러 쉽게 삭제할 수 있다
로컬 리포지토리에서 브랜치 삭제는 git branch -d <브랜치명> 으로 할 수 있다
git branch -d feat/todo
Git은 버전 관리를 위해서 브랜치가 합쳐지지 않으면 삭제하지 못 하도록 설정되어 있다 - 미완성 기록을 삭제하고 싶을 경우에 -D 옵션을 쓰면 삭제할 수 있다 - 머지되지 않은 브랜치 삭제는 버전 기록 시스템의 사용 목적과는 맞지 않는다 - 미완성 기록이라도 해당 기능으로 돌아가야 할 수도 있으므로 남겨두는 것을 권장한다
git branch -D feat/todo
6. Git 문제 발생 대처안
7. Git Error 대처방법
1) reject error
! [rejected] master -> master (non-fast-forward))
git push 또는 git push origin main 등의 명령을 실행해도 reject를 동반한 에러가 발생할 경우 대처 방법이다
! [rejected] master -> master (non-fast-forward)
push 하고져 하는 디렉토리에서
git init
로 초기화 한 후
git remote add origin {git push 주소}
로 다시 연결을 진행한다
다시
git push 또는 git push origin main
을 하여도 에러가 발생한다면...
git push origin +main
처럼 main 앞에 '+'를 붙여서 진행한다
웹 애플리케이션에서는 HTTP 메서드를 이용해 서버와 통신한다 - GET을 통해 웹 페이지나 데이터를 요청한다 - POST로 새로운 글이나 데이터를 전송한다 - DELETE로 저장된 글이나 데이터를 삭제한다
클라이언트와 서버가 HTTP 통신을 할 때는 어떤 요청을 보내고 받느냐에 따라 메서드의 사용이 달라진다
REST API에서 REST는 “Representational State Transfer”의 약자이다 - 로이 필딩의 박사학위 논문에서 웹(http)의 장점을 최대한 활용할 수 있는 아키텍처로써 처음 소개되었다
REST API는 웹에서 사용되는 데이터나 자원(Resource)을 HTTP URI로 표현하고, HTTP 프로토콜을 통해 요청과 응답을 정의하는 방식이다
2) 역활
클라이언트와 서버 사이에는 데이터와 리소스를 요청하고 요청에 따른 응답을 전달하기 위한 도구 필요하다
도구를 통해 클라이언트는 서버에 필요한 것을 요청하고, 이에 대한 응답을 다시 서버에서 클라이언트로 전송할 수 있다
HTTP 프로토콜 기반으로 요청과 응답에 따라 리소스를 주고받는 작업을 API가 수행해야 한다 - 클라이언트와 서버 간에 서로 잘 알아볼 수 있도록 작성하는 것이 중요하다
2. REST API를 디자인하는 방법
REST API를 작성에는 몇 가지 지켜야 할 규칙들이 있다
로이 필딩이 논문에서 제시한 REST 방법론을 보다 더 실용적으로 적용하기 위해 레오나르드 리차드슨은 REST API를 잘 적용하기 위한 4단계 모델을 만들었다 - 로이 필딩은 이 모델의 모든 단계를 충족해야 REST API라고 부를 수 있다고 주장했다 - 2단계까지만 적용해도 좋은 API 디자인이라고 볼 수 있고, 이런 경우 HTTP API 라고 한다 - 리차드슨의 REST 성숙도 모델은 총 4단계(0~3단계)로 나누어진다
1) REST 성숙도 모델 - 0단계
0단계에서는 HTTP 프로토콜을 사용하기만 해도 된다 - 0단계의 경우에는 해당 API를 REST API라고 할 수는 없다 - 0단계는 좋은 REST API를 작성하기 위한 기본 단계이다
허준이라는 의사의 예약 가능한 시간을 확인하고, 어떤 특정 시간에 예약하는 상황의 예제 - HTTP 프로토콜을 사용하고 있다 - 단순히 HTTP 프로토콜을 사용하는 것이 REST API의 출발점이다
REST API는 웹에서 사용되는 모든 데이터나 자원(Resource)을 HTTP URI로 표현한다 - 모든 자원은 개별 리소스에 맞는 엔드포인트(Endpoint)를 사용해야 한다 - 모든 자원은 요청하고 받은 자원에 대한 정보를 응답으로 전달해야 한다
0단계에서는 모든 요청에서 엔드포인트로 /appointment 를 사용하였다
1단계에서는 요청하는 리소스가 무엇인지에 따라 각기 다른 엔드포인트로 구분하여 사용해야 한다
1단계 예제 - 예약 가능 시간 확인이라는 요청의 응답으로 받게 되는 자원(리소스)은 허준이라는 의사의 예약 가능 시간대이다 : 예약 가능 시간 확인 요청 시 /doctors/허준이라는 엔드포인트를 사용하였다 - 특정 시간에 예약하게 되면, 실제 slot이라는 리소스의 123이라는 id를 가진 리소스가 변경된다 : 특정 시간 예약이라는 요청에서는 /slots/123으로 실제 변경되는 리소스를 엔드포인트로 사용하였다
어떤 리소스를 변화시키는지 혹은 어떤 응답이 제공되는지에 따라 각기 다른 엔드포인트를 사용해야 한다 - 적절한 엔드포인트를 작성하는 것이 중요하다 - 엔드포인트 작성 시에는 동사, HTTP 메서드, 어떤 행위에 대한 단어 사용은 지양한다 - 리소스에 집중된 명사 형태의 단어로 작성하는 것이 적당하다
요청에 따른 응답으로 리소스를 전달할 때에도 사용한 리소스에 대한 정보와 리소스 사용에 대한 성공/실패 여부를 반환해야 한다 - 김코딩 환자가 허준 의사에게 9시에 예약을 진행하였으나, 해당 시간에 예약이 불가능할 경우에는 리소스 사용에 대한 실패 여부를 포함한 응답을 받아야 한다
{ "appointmentFailure" : { "slots" : { "id" : 123, "doctor" : "허준", ... } , "patient" : "김코딩" , "reason" : "해당 시간은 이미 다른 예약이 있습니다" } }
3) REST 성숙도 모델 - 2단계
2단계에서는 CRUD에 맞게 적절한 HTTP 메서드를 사용하는 것에 중점을 둔다
0단계와 1단계 예시에서는 모든 요청을 CRUD에 상관없이 POST로 하고 있다 - REST 성숙도 모델 2단계에 따르면 CRUD에 따른 적합한 메서드를 사용한 것이 아니다
예약 가능 시간 확인은 예약 가능한 시간을 조회(READ)하는 행위를 의미한다 - 조회(READ)하기 위해서는 GET 메서드를 사용하여 요청을 보낸다 - GET 메서드는 body를 가지지 않기 때문에 query parameter를 사용하여 필요한 리소스를 전달한다
특정 시간 예약은 해당 특정 시간에 예약을 생성(CREATE)한다는 것과 같다 - 예약을 생성(CREATE)하기 위해서는 POST 메서드를 사용하여 요청을 보내는 것이 적당하다
2단계에서는 POST 요청에 대한 응답이 어떻게 반환되는지도 중요하다 - 응답은 새롭게 생성된 리소스를 보내주기 때문에, 응답 코드도 201 Created 로 명확하게 작성해야 한다 - 관련 리소스를 클라이언트가 Location 헤더에 작성된 URI를 통해 확인할 수 있어야 완벽하게 REST 성숙도 모델의 2단계를 충족한 것이다
응답 내에 새로운 링크를 넣어 새로운 기능에 접근할 수 있도록 하는 것이 3단계의 중요 포인트이다 - 예약 가능 시간을 확인한 후에는 그 시간대에 예약을 할 수 있는 링크를 삽입할 수 있다 - 특정 시간에 예약을 완료하고 나서 예약을 다시 확인할 수 있도록 링크를 작성할 수 있다
두 클래스를 상위 클래스와 하위 클래스로 나누어 상위 클래스의 멤버(필드, 메서드, 이너 클래스)를 하위 클래스와 공유하는 것을 의미한다
여기서 우리는 이 두 클래스를 서로 상속 관계 있다고 하며, 하위 클래스는 상위 클래스가 가진 모든 멤버를 상속받게 됩니다.
하위 클래스의 멤버 개수는 상위 클래스의 멤버 개수보다 같거나 많아야 한다
상위 클래스-하위 클래스의 관계를 조상-자손 관계로 표현하기도 한다
두 클래스 간 상속 관계를 설정할 때 사용하는 extends 키워드는 "확장하다"라는 의미를 가지고 있다
상위 클래스와 하위 클래스의 공통점과 다른점이 있다 - 상위 클래스의 변수와 메서드를 하위 클래스에 그대로 받아와서 사용한다 - 하위 클래스는 필요한 변수와 메서드를 추가하여 코드를 작성할 수 있다
상속을 통해 클래스를 작성하면 코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있다
상속은 다형적 표현이 가능하다는 장점이 있다
하위 객체의 클래스에 상위 객체에서 확장된 클래스의 성격도 가지고 있는 것을 다형성이라 한다
위의 그림을 코드로 작성하면 아래와 같다 - extends 를 확장 메서드로 사용한다
class Person{
String name;
int age;
void learn(){
System.out.println("study");
}
void walk(){
System.out.println("walking");
}
void eat(){
System.out.println("eating");
}
}
class Teacher extends Person{
String schoolName;
void teach(){
System.out.println("teaching");
}
}
class Ceo extends Person{
String companyName;
void approval(){
System.out.println("review");
}
}
class Developer extends Person{
String groupName;
void coding(){
System.out.println("coding");
}
}
public class Extends {
public static void main(String[] args) {
Person p = new Person();
p.name = "kms";
p.age = 25;
p.learn();
p.walk();
p.eat();
System.out.println(p.name);
Teacher t = new Teacher();
t.name = "kim";
t.age = 30;
t.learn();
t.walk();
t.eat();
t.teach();
System.out.println(t.name);
}
}
코드의 결과는 다음과 같이 출력된다 - Teach 클래스가 person 클래스의 변수와 메서드를 상속받아서 확장된 것을 확인할 수 있다 - extends 메서드로 상위 클래스를 확장하지 않았다면 중복된 변수와 메서드를 반복해서 작성해야 했을 것이다
2. 포함(composit)
포함(composite)은 상속처럼 클래스를 재사용할 수 있는 방법이다
클래스의 멤버로 다른 클래스 타입의 참조변수를 선언한다
클래스명(멤버1, 멤버2, 멤버3,...) 의 형태로 사용한다 - 외부 클래스 내에 내부 클래스를 작성한다 - 내부 클래스에 main 클래스가 있다 - Address 객체를 외부 클래스 밖에서 별도의 클래스로 선언 한다
public class Composit {
int id;
String name;
Address address;
public Composit(int id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
void showInfo() {
System.out.println(id + " " + name);
System.out.println(address.city + " " + address.country);
}
public static void main(String[] args) {
Address address1 = new Address("Gangnamgu", "Seoul");
Address address2 = new Address("Junggu", "Seoul");
Composit e = new Composit(1, "kms", address1);
Composit e2 = new Composit(2, "kim", address2);
e.showInfo();
e2.showInfo();
}
}
class Address {
String city, country;
public Address(String city, String country) {
this.city = city;
this.country = country;
}
}
Address 클래스에 포함되어 있는 인스턴스 변수 city와 country를 각각 Composit 클래스의 변수로 정의해주어야 하지만, Address 클래스로 해당 변수들을 묶어준 후 Composit 클래스 안에 참조변수를 선언하였다 - 코드의 중복을 없애고 포함관계로 재사용하는 방법이다
3. 상속과 포함의 선택
클래스 간의 관계가 ‘~은 ~이다(IS-A)’ 의 관계이면 상속을 사용한다
클래스 간의 관계가 '~은 ~을 가지고 있다(HAS-A)'의 관계이면 포함을 사용한다
4. 메서드 오버라이딩(Method Overriding)
상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것을 의미한다
Override는 사전적으로 "~위에 덮어쓰다"를 의미한다
1) 일반적인 메서드오버라이딩
class Airplane{
void fly(){
System.out.println("Airplane is flying");
}
}
public class MethodOverriding extends Airplane {
void fly(){
System.out.println("MethodOverriding is flying");
}
public static void main(String[] args) {
MethodOverriding over = new MethodOverriding();
over.fly();
}
}
MethodOverriding 클래스가 Airplane 클래스의 fly 메서드를 오버라이딩 한 후 MethodOverriding 클래스에 적합하게 fly 메서드를 변경하고 있다
메서드 오버라이딩은 세가지 조건을 반드시 만족해야 한다 - 메서드의 선언부(메서드 이름, 매개변수, 반환타입)가 상위클래스와 완전히 일치해야 한다 - 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다 - 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다
2) 상위 클래스의 타입을 적용한 메서드 오버라이딩
public class Overriding {
public static void main(String[] args) {
Bike bike = new Bike();
Car car = new Car();
Human human = new Human();
bike.run();
car.run();
human.run();
//상위 클래스 Vehicle 타입을 사용하여 각각의 객체의 메서드 값을 다르게 출력한다
Vehicle bike2 = new Bike();
Vehicle car2 = new Car();
Vehicle human2 = new Human();
bike2.run();
car2.run();
human2.run();
}
}
class Vehicle{
void run(){
System.out.println("Vehicle is running");
}
}
class Bike extends Vehicle{
void run(){
System.out.println("Bike is running");
}
}
class Car extends Vehicle{
void run(){
System.out.println("Car is running");
}
}
class Human extends Vehicle{
void run(){
System.out.println("Human is running");
}
}
3) 배열을 적용한 메서드 오버라이딩
Vehicle[] vehicles = new Vehicle[]{new Bike(), new Car(), new Human()};
for (Vehicle vehicle : vehicles) {
vehicle.run();
}
??? 이해가 잘 안간다...추가로 찾아보자.
5. super
1) super
자신보다 상위의 클래스에 있는 객체를 의미한다
반드시 함수의 첫 줄에 위치하여야 한다
ex1) Parents의 변수 count와 Kim의 변수 count가 동일한 상황에서의 Kim 과 Parents의 나이를 출력
class
Kim
Parents
variable
age
count
count
constructor
Kim()
- #19 에서 굳이 this 키워드를 사용하지 않아도 출력은 동일하다. 설명을 위하여 구분하기 위해 사용하였다
- 여기서 super 과 this() 의 차이점은 각각 상위클래스의 객체와 생성자를 의미한다는 부분이다
-상속관계를 전제로 하며, super.변수명 의 형식으로 사용한다
※ 상속관계가 아닐 경우 에러(count 변수를 찾을 수 없다)가 발생한다
2) super()
super은 객체를 의미하지만, super()는 생성자를 의미한다
반드시 함수의 첫 줄에 위치하여야 한다
ex1) Parentsname의 변수 count와 Kimname의 변수 count가 동일한 상황에서의 Kimname 과 Parentsname의 나이를 출력하고, Parentsname를 상속받아 모두 출력
class
Kimname
Parentsname
variable
name
count
constructor
Kimname
Parentsname
6. Object 클래스
자바 클래스 상속계층도 중 최상위에 위치한 상위클래스이다
자바의 모든 클래스는 Object클래스를 기본적으로 상속되면, Object클래스로부터 확장된다
상속클래스가 없는 경우 자동으로 extends object를 추가하여 Object클래스가 상속된다
class Parent //컴파일러가 "extends Object" 자동 추가 {
}
class Child extends Parent {
}
변수는 클래스 변수(cv, class variable), 인스턴스 변수(iv, instance variable), 지역 변수(lv, local variable) 세 가지로 구분될 수 있다
필드는 클래스 변수와 인스턴스 변수이며, static 키워드의 유무로 구분할 수 있다 - static 키워드가 함께 선언된 것은 클래스 변수이다 - static 키워드가 없는 것은 인스턴스 변수이다
두 가지 변수 유형에 포함되지 않고 메서드 내에 포함된 모든 변수를 지역변수라고 한다
세 가지 유형의 변수들은 주로 선언된 위치에 따라 그 종류가 결정되며 각각 다른 유효 범위(scope)를 가진다
class Area { //클래스 영역 시작
int instanceVariable; // 인스턴스 변수
static int classVariable; // 클래스 변수(static 변수, 공유변수)
void method() {
// 메서드 영역 시작
int localVariable = 0; // 지역 변수. 속해있는 {}블록 안에서만 유효
} //메서드 영역 끝
}//클래스 영역 끝
1) 인스턴스 변수(iv)
인스턴스가 가지는 각각의 고유한 속성을 저장하기 위한 변수이다
new 생성자() 를 통해 인스턴스가 생성될 때 만들어진다
클래스를 통해 만들어진 인스턴스는 힙 메모리의 독립적인 공간에 저장된다
동일한 클래스로부터 생성되었지만 객체의 고유한 개별성을 가진다
사람마다 성별, 이름, 나이, MBTI가 다 다르듯 인스턴스 변수는 그 고유한 특성을 정의하기 위한 용도로 사용한다
2) 클래스 변수(cv)
static 키워드를 통해 선언한다
클래스 변수는 독립적인 저장 공간을 가지는 인스턴스 변수와 다르게 공통된 저장공간을 공유한다
한 클래스로부터 생성되는 모든 인스턴스 들이 특정한 값을 공유해야하는 경우에 주로 static 키워드를 사용하여 클래스 변수를 선언한다
사람을 예로 들면 손가락과 발가락 개수와 같이 모든 사람이 공유하는 특성을 저장하는 데에 사용된다
클래스 변수는 인스턴스 변수와 달리 인스턴스를 따로 생성하지 않고 '클래스명.클래스변수명' 을 통해 사용이 가능하다
area.classVariable 처럼 클래스 변수를 사용할 수 있다
3) 지역변수(lv)
지역변수는 메서드 내에 선언된다
메서드 내({} 블록)에서만 사용가능한 변수이다
멤버 변수와는 다르게 지역변수는 스택 메모리에 저장되어 메서드가 종료되는 것과 동시에 함께 소멸되어 더이상 사용할 수 없게 된다
힙 메모리에 저장되는 필드 변수는 객체가 없어지지 않는 한 절대로 삭제되지 않지만, 스택 메모리에 저장되는 지역변수는 한동안 사용되지 않는 경우 가상 머신에 의해 자동으로 삭제된다
4) 필드변수와 지역변수의 차이
필드 변수와 지역 변수의 주요한 한 가지 차이점은 초기값에 있다
지역변수는 직접 초기화하지 않으면 값을 출력할 때에 오류가 발생한다
필드 변수는 직접적으로 초기화를 실행하지 않더라도 강제로 초기화가 이뤄진다
메모리의 저장 위치와 긴밀한 연관성을 가진다
힙 메모리에는 빈 공간이 저장될 수 없기 때문에 저장되는 필드는 강제로 초기화된다
스택 메모리는 강제로 초기화되지 않으므로 지역 변수는 선언시 반드시 초기화를 실행해야 한다
2. static & instance 변수
1) static 변수
공유의 개념을 가지고 있다
클래스 변수를 생성하는데 사용한다
예제1)
public class StaticTest0 {
public static void main(String[] args) {
}
class Car {
public String str1 = "Instance Variable";
public static String str2 = "class Variable";
public static void method() {
System.out.println(str1);
System.out.println(str2);
}
public void method() {
System.out.println(str1);
System.out.println(str2);
}
}
}
<결과>
java: non-static variable str1 cannot be referenced from a static context
java: 정적 컨텍스트에서 비정적 변수 str1을 참조할 수 없습니다.
- 위의 코드에서 어느 부분에서 에러가 발생할까?
- public static void method() { System.out.println(str1); : 출력 명령에서 발생한다 : str1은 인스턴스 변수로써 클래스 메서드 내에서는 참조를 할 수 없다 : 클래스 메서드 내에서는 클래스 변수만이 참조가 가능하다
예제2)
public class StaticTest1 {
public static void main(String[] args) {
Car.method1();
Car.method2();
System.out.println(Car.str1);
System.out.println(Car.str2);
}
class Car {
public String str1 = "Instance variable";
public static String str2 = "class variable";
public static void method1() {
// System.out.println(str1);
System.out.println(str2);
}
public void method2() {
System.out.println(str1);
System.out.println(str2);
}
}
}
java: non-static method method2() cannot be referenced from a static context
java: 정적 메서드2는 정적 컨텍스트에서 참조할 수 없습니다.
- Car.method2(); System.out.println(Car.str1); : 참조와 출력 명령에서 에러가 발생한다 : method2 메서드는 인스턴스 메서드이므로 공유가 되지 않는다 : str1 역시 인스턴스 변수로써 공유가 되지 않는다 - 인스턴스 메서드를 참조하고져 하면 인스턴스를 new 연산자로 생성하여야 한다
예제 3) 클래스 변수의 내용을 변경할 경우 전체가 변경된다
public class StaticTest1 {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
car1.cv = "car1.str1 = Instance variable";
System.out.println(car1.cv);
System.out.println(car2.cv);
System.out.println(Car.cv);
}
static class Car {
public String iv = "Instance variable";
public static String cv = "class variable";
public static void method1() {
System.out.println(cv);
}
}
}
예제 4) 인스턴스 변수의 내용을 변경할 경우 변경한 iv만 적용된다
public class StaticTest1 {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
car1.iv = "car1.str1 = Instance variable";
System.out.println(car1.iv);
System.out.println(car2.iv);
// System.out.println(Car.iv);
}
static class Car {
public String iv = "Instance variable";
public static String cv = "class variable";
public static void method1() {
// System.out.println(iv);
}
}
}
.length 메서드는 index 값으로 변황하는 역활을 한다 - index로 변환하여 해당 위치에 있는 배열의 값을 환원한다
package array;
public class Test1 {
public static void main(String[] args) {
//scores 변수에 5개의 int 값을 배정한다
int[] scores = {100, 90, 85, 95, 100};
int sum =0; //합계를 구하기 위하여 sum 변수를 0으로 지정한다
//scores.length로 배열의 index값인 0~4까지를 지정한다
for (int i=0; i<scores.length; i++) {
sum = sum + scores[i];
/* 0 = 0 + 100
190 = 100 + 90
275 = 190 + 85
370 = 275 + 95
470 = 370 + 100
*/
}
System.out.println(sum); //최종 sum 값이 출력된다
}
}
2) array와 while를 사용한 코드
package array;
public class ArrayWhile {
public static void main(String[] args) {
int[] scores = {100, 90, 85, 95, 100};
int sum=0;
int i=0;
while(i<scores.length){
sum = sum + scores[i++];
}
System.out.println(sum);
}
}