2024.09.20.금.TIL Validation

김기남·2024년 9월 20일
post-thumbnail

안녕하세요, 오늘은 Validation 에 대해 정리해보았습니다.

Validation

💡 Controller의 주요한 역할 중 하나는 Validation 이다. 즉, HTTP 요청이 정상인지 검증하는것이다.

Validation은 왜 해야하는가??

  1. 만약 주문서 작성 페이지에서 잘못된 입력값으로 인해 서버에 오류가 발생한다면?
    1. ex) 휴대폰 번호에 숫자가 아닌 문자가 들어간 경우 → BadRequest
  2. 서버의 문제로 인해 작성 페이지에서 Error 페이지로 이동된다면?
    1. 위 예시의 경우 Bad Request Response와 오류페이지가 반환
  3. Error 페이지로 이동되어 작성중인 폼이 모두 리셋되어 다시 작성해야 한다면?
    1. 유저 : 나 안해..
  • 이러한 서비스의 유저는 굉장한 불편함을 겪게된다.
  • 검증을 통해 적절한 메세지를 유저에게 보여주어야 한다.
  • 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 한다.
  • 사용자가 입력한 데이터는 유지된 상태여야 한다.
  • 실제 서버를 운영하다보면 정말 기능의 의도와 다른 별의별 사용방법들을 보게된다..
    • ex) Enter로 입력이 완료되도록 만들었지만 누군가는 Click, Tab + Enter를 누르듯..
    • 서비스 출시전 QA를 진행하는 이유가 다 있습니다.
      • 시나리오별 테스트
        • 1번의 경우 → 과정 → 결과(성공)
        • 2번의 경우 → 과정 → 결과(실패) → 수정 → 성공까지 반복

Front 검증

  • 해당 검증은 유저가 조작할 수 있음으로 보안에 취약하다.
    • Javascript를 이용한 Validation
  • 보안에 취약하지만 그럼에도 꼭 필요하다
    • ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가

Server 검증

  • Front 검증없이 서버에서만 검증한다면 유저 사용성이 떨어진다.
    • Front 검증과 적절히 섞어서 사용해야 한다.
  • API Spec을 정의해서 Validation 오류를 Response 예시에 남겨주어야 한다.
    • 뉴스피드 프로젝트를 진행하며 API 명세서를 만든것은 좋은 경험이다.
    • 실제로 크게는 B2B 혹은 B2C 작게는 프론트 개발자와 백엔드 개발자 모두
      API 명세서만으로 일하는 경우가 많다. → Spec을 정말 잘 만드는것이 좋다.
  • 서버 검증은 필수이다..!

결론

프론트 + 서버 + DB 모두 검증을 꼼꼼하게 하는것이 바람직하다.
Validation으로 많은 Error와 문제들을 방지할 수 있다.

BindingResult


💡 Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체이다. `@ModelAttribute`를 이용해 매개변수를 바인딩 할 때 발생한 오류를 보관하는 객체 바인딩이 실패하면 기본적으로 400 BadRequest Error가 발생한다.
  • 주의 : BindingResult 파라미터의 위치는 @ModelAttribute 파라미터 뒤에 와야한다.
@Data
public class CreateRequestDto {
		private Long field1;
		private String field2;
		private Integer field3;
}

@Controller
public class BingdingResultController {
	
	@PostMapping("/create")
	public String create(
				@ModelAttribute CreateRequestDtp request, // 1. @ModelAttribute 뒤에
				BindingResult bindingResult // 2. BindingResult
	) {

		if(!StringUtils.hasText(item.getField2())) { // 3. Validation
			
			bindingResult.addError( // 4. Error 추가
				new FieldError("request", "field2", "field2는 필수 입니다.")
			)

		}
		return "viewName";
	}

}

BindingResult가 없는 경우

  • 예시코드
@Data
public class CreateRequestDto {
		private Long field1;
		private String field2;
		private Integer field3;
}

@Controller
public class BingdingResultController {
	
	@PostMapping("/create")
	public String create(@ModelAttribute CreateRequestDto request) {
		// logic
		return "viewName";
	}

}
  • Postman
    • 정상 요청

- 정상 적인 응답이 온다.
- viewName 페이지로 이동한다.

- 잘못된 요청

- 바인딩 오류가 발생한다.
- 검증 오류(400 Bad Request)가 발생하고 Controller가 호출되지 않는다.
- Error Page로 이동된다.

BindingResult가 있는 경우

@Data
public class CreateRequestDto {
		private Long field1;
		private String field2;
		private Integer field3;
}

@Controller
public class BingdingResultController {
	
	@PostMapping("/create")
	public String create(
				@ModelAttribute CreateRequestDtp request, 
				BindingResult bindingResult
	) {
		// logic
		return "viewName";
	}

}
  • Postman

    • 정상 요청

    • 정상 적인 응답이 온다.

    • viewName 페이지로 이동한다.

    • 잘못된 요청

    • @ModelAttribute 필드 or 객체에 파라미터 바인딩 오류가 발생

      • BindingResult에 오류가 보관되고 Controller는 정상적으로 호출된다.

파라미터에 @ModelAttribute와 Binding Result가 함께 있는 경우

@ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다.
만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고
나머지 필드들만 바인딩 된 후 Controller가 호출된다.

정리

현재 우리는 대부분 RestAPI를 만들고있고 Bean Validation을 사용하기 때문에
위의 방법(@ModelAttribute, BindingResult)은 깊게 다루지않고 넘어가겠습니다.
시간은 많이 없으나 알려드려야할것이 너무 많아요!!(쿠키, 세션, 필터, 인터셉터, JPA, Docker 등등..)
하지만, BindingResult는 Spring의 꽤 중요한 주요 기능이니 한번쯤 공부해주셔야 합니다.

Bean Validation


💡 검증 기능을 매번 BingdingResult 예시처럼 코드로 구현하는것은 번거로운 일이다. 특정 필드 검증의 경우 빈값, 길이, 크기, 형식 과 같은 간단한 로직이다. 이러한 로직들을 모든 프로젝트에 적용할 수 있도록 표준화 한 것이 Bean Validation이다.
  1. Bean Validation은 하나의 기술 표준 인터페이스이다.
    1. 다양한 Validation Annotation들과 여러가지 Interface로 구성되어 있다.
    2. JPA(인터페이스)의 구현체로 하이버네이트가 존재하고 대부분이 사용하듯
      Bean Validation(인터페이스) 구현체인 하이버네이트 Validator를 사용한다.
      - ORM JPA Hibernate와는 상관없음. 이름만 하이버네이트
  2. BindingResult, FieldError, ObjectError를 통해 Controller에서 값을 검증할 수 있지만,
    이 경우 Controller의 크기가 너무 커지고 단일 책임 원칙에 위배된다.

Hibernate Validator 공식문서

💡 **공식 사이트**: https://hibernate.org/validator/ **버전별 공식 메뉴얼**: https://docs.jboss.org/hibernate/validator
  • 모든 기술의 정확한 사용법은 공식문서에 있다.
  • 영어 원서를 읽는것이 익숙하지 못해도 똑똑한 AI 형들이 많으니 자신있게 찾아보자
  • 코드예시
@Getter
public class SignUpRequestDto {
	
	@NotBlank
	private String name;

	@NotNull
	@Range(min = 1, max = 120)
	private Integer age;

}
  • 어노테이션을 적용시키는것 만으로 Validation을 아주 쉽게 적용할 수 있다.
  • Controller에 개발자가 기본적인 검증 로직을 작성할 필요가 없어졌다.
  • 이것이 Bean Validation

참고

💡 Spring Boot 는 build.gradle에 라이브러리 이름만 추가하면 자동으로 내 프로젝트에 맞는 라이브러리 버전을 적용하도록 도와주는 기능이 있다. 거꾸로 말하면 원래는 수동 이였다.
  • Gradle

    💡 Gradle은 JVM 기반의 오픈 소스 빌드 자동화 도구이다.(빌드 관리 도구) 프로젝트의 빌드, 테스트, 배포를 관리하는 데 사용된다. Groovy나 Kotlin DSL(Domain Specific Language)을 사용하여 **빌드 스크립트를 작성하며, 프로젝트의 의존성 관리, 태스크 정의, 플러그인 사용 등**을 통합적으로 제공한다. - 간단히 정리하자면 소프트웨어 개발 프로세스를 자동화하고 효율적으로 관리할 수 있게 해주는 도구
  • build.gradle

  • SpringBoot, Java의 Version이 선언되어 있다.

  • 라이브러리의 Version은 따로 명시해주지 않았다.

  • build 실행시

Bean Validation 사용


💡 단순히 어노테이션을 선언해주면 검증이 완료되는 이유는 **Validator**(Validation을 사용하는것)가 존재하기 때문이다.

Field Error

  • 코드예시
    • 의존성 추가(build.gradle)

      implementation 'org.springframework.boot:spring-boot-starter-validation'

      implementation 뒤에 version이 따로 적혀있지 않은 이유 → Spring Boot 기능

    • 파일경로 jakarta.validation-api-${version}.jar

      • jakarta.validation

  • constraints 경로에서 다양한 어노테이션들을 확인할 수 있다.

  • org.hibernate.validator:hibernate-validator:${version}
    → 위 validation들이 동작하게 만들어주는 Validator

  1. jakarta.validation-api : Interface
  2. org.hibernate.validator:hibernate-validator : 구현체
  • 실제코드 import

  • @Range 어노테이션은 Hibernate Validator 에서만 동작하는것

  • @NotBlank, @NotNull 은 validation 표준 인터페이스

  • 참고

@NotBlank

  1. null을 허용하지 않는다.
  2. 공백(” “)을 허용하지 않는다. 하나 이상의 문자를 포함해야한다.
  3. 빈값(””)을 허용하지 않는다.
  4. CharSequence 타입 허용 → String은 CharSequence(Interface)의 구현체

@NotNull

  1. null을 허용하지 않는다.
  2. 모든 타입을 허용한다.

@NotEmpty

  1. null을 허용하지 않는다.
  2. 빈값(””)을 허용하지 않는다.
  3. CharSequence, Collection, Map, Array 허용
  • 테스트 코드 예시
@Data
public class TestDto {
	
	@NotBlank // 테스트 하고싶은 어노테이션으로 변경
	private String stringField;
	
	@NotNull
	@Range(min = 1, max = 9999)
	private Integer integerField;
	

}

public class BeanValidationTest {
	
	@Test
	void beanValidation() {
		
		// Spring과 통합하면 아래 두줄의 코드는 사용하지 않는다.
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		Validator validator = factory.getValidator();
		
		TestDto dto = new TestDto();
		// Test 하고싶은 상황을 만들어서 검증해보자
		dto.setStringField(" ");
		dto.setIntegerField(1);
		
		Set<ConstraintViolation<TestDto>> violations = validator.validate(dto);

		for(ConstraintViolation<TestDto> violation : violations) {
			// 1. 아래의 결과에 Message가 있으면 Validation에 걸린것.
			// Default Message가 있기 때문에 출력됨
			// Message를 수정하고싶다면 어노테이션 속성값(message="입력")으로 설정할 수 있다.
			// 2. 결과가 비어있으면 Validation에 걸리지 않은것.
			System.out.println("violation = " + violation.getMessage());
		}
		
	}

}

위 코드로 하나하나 검증해볼 수 있다!

참고 : org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능
즉, 다른 구현체로 validator를 교체하였을 경우 동작하지 않는다.
하지만 대부분 org.hibernate.validator를 대부분 사용한다.

Spring은 어떻게 Bean Validation을 사용하는가


💡 Spring Boot는 validation 라이브러리를 설정하면 `'org.springframework.boot:spring-boot-starter-validation'` 자동으로 Bean Validator를 스프링에 통합되도록 설정해준다.
  1. LocalValidatorFactoryBean 을 Global Validator로 등록한다.

    • Class Diagram

  • 주의
    • Global Validator를 수동으로 등록하면 LocalValidatorFactoryBean 을 등록하지 않는다.
  1. Global Validator가 Default로 적용되어 있으니 @Valid, @Validated 만 적용하면 된다.
    1. @Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 어노테이션이다.
    2. @Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.
    3. @ValidMethodArgumentNotValidException 예외를 발생시킨다.
    4. @ValidatedConstraintViolationException 예외를 발생시킨다.
  2. Bean Validation Annotation이 있으면 검증을 수행한다.
    1. ex) @NotNull, @NotBlank, @Max 등등..
  3. Validation Error가 발생하면 FieldError, ObjectError를 생성하여 BindingResult에 담아준다.

동작 순서

  1. @ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도

    1. 성공시 다음 동작
    2. 실패시 typeMismatch FieldError 발생
  2. Validator 적용

    1. @ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용
      • Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.(위에 나옴)
      • 타입 바인딩(변환)에 성공한 필드만이 Bean Validation을 적용하는 의미가 있다.
      • ex) Integer 타입 필드에 문자가 오면 애초에 검증의 의미가 없다.
      • 성공한 경우 : String 필드에 문자입력 → 바인딩 성공 → String 필드에 Bean Validation 적용
      • 실패한 경우 : Integer 필드에 문자입력 → 바인딩 실패 → typeMismatch FieldError 추가
        → 바인딩에 실패한 필드는 애초에 값이 들어가있지 않음(null) → Bean Validation 적용하지 않음

에러 메세지


💡 Bean Validation이 Default로 제공하는 에러 메세지를 변경하고 싶다면?
  • Bean Validation을 적용하고 bindingResult에 등록된 검증 오류를 확인해보면
    오류가 어노테이션 이름으로 등록되어 있다.

  • Spring 에서는 오류 메시지 코드관리를 위해 MessageCodesResolver 인터페이스의
    구현체인 DefaultMessageCodesResolver를 기본으로 사용한다.

  • 예시코드

@Data
public class TestDto {

	@NotBlank
	private String field1;
	
	@NotNull
	private Integer field2;

}



@Slf4j
@Controller
public class BeanValidationController {

	@PostMapping("/error-message")
	public void test(
			@Validated @ModelAttribute TestDto dto, 
			BindingResult bindingResult
	) {
		
		if(bindingResult.hasErrors()) {
			log.info("errors={}", bindingResult);
		}

	}

}
  1. @NotBlank , @NotNull 사용
  • Postman Request

    1. x-www-form-urlencoded : 작성 폼 제출
    2. field1은 비워두고(null) field2에만 값 입력
  • 출력결과

    Field error in object 'testDto' on field 'field1': rejected value [null]; codes [NotBlank.testDto.field1,NotBlank.field1,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.field1,field1]; arguments []; default message [field1]]; default message [공백일 수 없습니다]
    1. NotBlank 오류 코드를 기반으로 MessageCodesResolver 를 통해 메세지 코드 순서대로 생성

      NotBlank.Object.fieldName

      NotBlank.fieldName

      NotBlank.(java.lang.String) 필드 타입

      NotBlank

에러 메세지 우선순위

  1. 생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기

    • Spring의 Message 기능을 학습해보자! (따로 해주세요 쉬워요!)
    1. NotBlank.Object.fieldName
    2. NotBlank.fieldName
    3. NotBlank.(java.lang.String) 필드 타입
    4. NotBlank
  2. 애노테이션의 message 속성 사용

    @Data
    public class TestDto {
    
    	@NotBlank(message = "안돼 돌아가")
    	private String field1;
    	
    	@NotNull
    	private Integer field2;
    
    }
  3. 라이브러리가 제공하는 Default Message 사용 “공백일 수 없습니다”.

Object Error


💡 위에 나온 내용은 Bean Validation Field Error들이다. BeanValidation에서 Object Error는 어떻게 처리하는가?
  • 개발자들의 귀차니즘은 위대하다.

    • 기본적으로 나태한 인간의 본능때문에 다시는 작업하지 않도록 효율적으로 코드를 작성하기 좋아한다.
    • 슈퍼마리오 게임의 숨겨진 비밀..!
    • 항상 기획이 변경될까 두렵고 초조해서 확장가능한 코드를 작성한다.(효율 대박)

@ScriptAssert

@Data
@ScriptAssert
public class TestDto {

	@NotNull
	@Range(min = 10, max = 9999)
	private Integer field1;
	
	@NotNull
	@Max(999)
	private Integer field2;

	@NotBlank
	private String field3;

}
  • 실무에서는 @ScriptAssert 제약사항 때문에 사용하지 않는다.(실제 사용성은 떨어진다)

    • 실무에서는 훨씬 복잡한 Validation들이 필요하다. 대응이 안된다.
    • ex) 다른 객체끼리의 비교 Validation 혹은 DB조회 결과와 비교
  • Object Error의 경우 Java 코드로 직접 Validation 한다.

  • @ScriptAssert 사용 X
    - 요구사항 : field1 * field2 ≥ 10000

    @Slf4j

    @Controller
    public class BeanValidationController {

    @PostMapping("/error-message")
    public void test(
    		@Validated @ModelAttribute TestDto dto, 
    		BindingResult bindingResult
    ) {
    	
    	// 실패로직
    	if(dto.getField1() != null && dto.getField2() != null) {
    		
    		int result = dto.getField1() * dto.getField2();
    		
    		if(result > 10000) {
    			bindingResult.reject("totalMin", new Object[]{10000, result}, null);
    		}
    		
    		if(bindingResult.hasErrors()) {
    			log.info("errors={}", bindingResult);
    		}
    	}
    
    	// 성공로직 ...
    }

    }

    정리하자면 Object Error는 로직으로 구현하자!

Bean Validation의 충돌


💡 만약 검증 요구사항중 등록의 경우와 수정의 경우 다른 Validation이 적용된다면?

참고

수정 API를 만들 때 id값을 항상 Controller에서 받도록 구성한다.
하지만 HTTP 요청은 사용자가 임의로 변경하여 요청할 수 있음으로
항상 서버에서 최종적으로 추가 검증을 진행 해야한다.
ex) 게시글 수정시 요청자 본인이 쓴 글인지 확인한다

  • 저장시 Test 객체
    @Data
    public class Test {
    // 자동생성
    private Long id;

    @NotNull
    @Range(min = 10, max = 9999)
    private Integer field1;
    
    @NotNull
    @Max(999)
    private Integer field2;
    
    @NotBlank
    private String field3;
    
    // 생성자

    }

@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {

private final TestRepository testRepository;

@PostMapping("/save")
public void test(
		@Validated @ModelAttribute Test test
) {
	// Validation 성공시 repository 저장로직 호출
	testRepository.save(test);
}

}

예시
등록시에는 field2가 최대 999이지만 수정시에는 제한이 없다면?
등록시에는 Repository에 저장될 Entity의 Id값이 없어도 되지만 수정시에는 id 값이 필수
수정시 Test 객체

@Data
public class Test {

@NotNull // id값 필수 요구사항 반영
private Long id;

@NotNull
@Range(min = 10, max = 9999)
private Integer field1;

@NotNull

// @Max(999) 무제한 요구사항 반영
private Integer field2;

@NotBlank
private String field3;

}

@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {

private final TestRepository testRepository;

@PutMapping("/{id}/edit")
public void test(
		@PathVariable Long id,
		@Validated @ModelAttribute Test test
) {

	Test test = testRepository.findById(id);

	test.setField1(test.getField1());
	test.setField2(test.getField2());
	test.setField3(test.getField3());

	// repository 수정로직 호출 service.update()
	testRepository.update(id, test);

}

}

@NotNull이 id값에 추가되고 @Max 가 사라져서 저장시 정상적으로 동작하지 않는다.
객체 id값에 @NotNull 이 추가되었기 떄문에 저장시 Validation Error 발생
field2 값에 @Max 가 사라져서 저장시에도 제한없이 저장됨
등록과 수정시 요구사항에 충돌이 발생한다.
해결방법 1 : 저장할 Object를 직접 사용하지 않고 SaveDto, UpdateDto 따로 사용한다.
여러분들이 자연스럽게 사용하고 있는 방법.(따라서 설명은 생략)
바인딩 받는 Object(RequestDto)를 저장할 Object(Entity)와 별도로 사용한다.
해결방법 2 : BeanValidation의 groups 기능을 사용한다.
제공되는 기능

groups

💡 동일한 객체를 등록할 때, 수정할 때 각각 다르게 검증하는 기능
  • 코드예시
// 저장용 group
public interface SaveCheck {

}

// 수정용 group
public interface UpdateCheck {

}

// Test 객체
@Data
public class Test {

	@NotNull(groups = UpdateCheck.class) // 수정시에만 반영
	private Long id;

	@NotNull(groups = {SaveCheck.class, UpdateCheck.class}) // 둘다 적용
	@Range(min = 10, max = 9999, groups = {SaveCheck.class, UpdateCheck.class})
	private Integer field1;
	
	@NotNull
  @Max(999, groups = SaveCheck.class) // 저장시에만 반영
	private Integer field2;

	@NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) // 둘다 적용
	private String field3;

}

@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {
	
	private final TestRepository testRepository;

	@PostMapping("/save")
	public void test(
			@Validated(SaveCheck.class) @ModelAttribute Test test // 저장 속성값 설정
	) {
		// Validation 성공시 repository 저장로직 호출
		testRepository.save(test);
	}

	@PutMapping("/{id}/edit")
	public void test(
			@PathVariable Long id,
			@Validated(UpdateCheck.class) @ModelAttribute Test test // 수정 속성값 설정
	) {

		Test test = testRepository.findById(id);

		test.setField1(test.getField1());
		test.setField2(test.getField2());
		test.setField3(test.getField3());

		// repository 수정로직 호출 service.update()
		testRepository.update(id, test);

	}

}
  • @Validated

    • 속성값이 존재한다.
    • spring이 제공하는 어노테이션
  • @Valid 에는 속성값이 존재하지 않는다, groups 기능 지원하지 않는다.

    • groups 기능을 사용하려면 @Validated를 사용해야 한다.

정리

groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
하지만 가독성이 떨어지고 코드 복잡도가 올라간다.
실무에서는 등록시 작성할 폼(화면)과 수정시 작성할 폼 객체(화면) 자체를 분리해서 사용하기 때문에
여러분이 사용하는 현재의 방법(DTO 분리 방법) 그대로 진행하면 된다.
네이밍은 일관성있게 작성해주세요.(UpdateRequestDto, SaveRequestDto)
그래도 우리가 왜 DTO 객체를 분리하여 사용하는지와 기능의 존재 여부는 알고있어야 합니다 여러분.

ex) 등록시에는 약관동의 등 도메인 관련 데이터 이외에 추가로 받는 데이터도 있다.
도메인 저장에 필요한 추가 데이터를 DB나 다른곳에서 조회하여 가져올 수 있다. 직접 해보면 압니다.

저장, 수정시 RequestDto가 비슷한데 하나로 합치는게 좋은가요?

  • 실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있다.
  • 각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있다..
  • RequestDto가 변한다는건 해당 API의 스펙 자체가 변경되는것임으로 많은 수정이 발생한다.
  • 실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하도록 하자..!

오늘의 코드카타

class Solution {

    public int solution(String myString, String pat) {
        int answer = 0;
        for (int i = myString.length() - 1; i >= 0; i--) {
            String subStr = myString.substring(0, i + 1);
            if (subStr.endsWith(pat)) {
                answer++;
            }
        }
        return answer;
    }
}
public class Main {

    public static void main(String[] args) {

        Solution solution = new Solution();

        System.out.println(solution.solution("banana", "ana"));
        System.out.println(solution.solution("aaaa", "aa"));

    }
}

문자열이 몇 번 등장하는지 세기

profile
새로운 시작~!

0개의 댓글