알게된 것
- display: flex 속성에 따라 어떻게 layout을 배치해야 하는지, 알게 되었다.
- 가로와 세로 방향을 설정할 수 있는데, 먼저 가로 방향의 경우에는
justify-Content:
- betweenSpace
- flex-start, end 등이 있고
align-Items:
- center
이렇게가 있었다.
예전에는 float: left, float: right인데
display: flex는 최근에 나온 스펙이라 그럴까 좀 더 쉽게 레이아웃하고, 뭔가 좀 딱 화면 최적화에 맞는 스펙인 것 같다. -> 찾아보니 맞음 mediaquery에 최적화되면서 html5, css3이랑 같이 나왔다고 함.
요즘 design을 하는 느낌이긴한데, 모던 html5디자인인가 싶기도 하고..
그리고 상태 관리하는 법에 대해서 알게 되었다.
redux에 대해서 알게 되었는데, dispatch해서 상태 값을 새로 업데이트 하고, useAppSelector써가지고 특정 저장소의 값을 꺼내오는 것 같더라.
결국 이것이 메모리에다가 값을 저장하는 것 같은데 맞을까? 리액트 메모리에 저장되는 거 ㅇㅇ, 이건 브라우저 스토리지 설정 정보에 따라 저장 기간이 달라지는 것일까 싶기도 하고, 리액트는 최대 얼마까지 저장할려나? 메가바이트 단위일려나? ㅇㅇ 보통 그렇다고 하고, 리액트가 브라우저 메모리를 공유한다고 함. 새로 고침되면 다 날라감.
 
다음에는, 화면을 figma에서 설계후, React 화면과 연동후 요걸 해보려고 한다!

  1. Figma에서 UI 만들기:
    • 페이지 구조, 버튼, 폼 필드, 카드 등을 설계.
    • Figma의 Auto Layout 기능을 활용해 반응형 디자인을 시뮬레이션하세요.
    • 디자이너가 만든 "디자인 시스템(Components)"을 참고하면 효율적이에요.
  2. 색상 및 스타일 시스템:
    • Primary / Secondary 컬러를 정의하세요.
    • 버튼, 텍스트 크기, 여백 등을 디자인 토큰으로 설정.
  3. CSS 코드 추출:
    • Figma는 CSS 스타일을 보여주는 기능이 있습니다.
    • 예: 텍스트 크기, 색상, 여백 등.
결제 기능 실제 연동하고, 배포하는 것도 해보기!

1장 - 본격적으로 공부하기 전, 기본적인 API 설계 원칙에 대한 개념을 숙지하자.

성공적인 웹 API는 기술적인 문제뿐만이 아닌 3가지의 관점에서 고려를 해야한다.
또한 잘 설계된 API란, 비즈니스적으로 얼마나 기여하는지?에 결정되며 결국 API가 직간접적으로
말하는 것은 조직에서 어떤 것을 가장 중요하게 생각하는지를 말해준다.

 

1. 비즈니스 관점

  • 비즈니스 환경에서 유의미한 기능을 제공하는 것 

2. 프로덕트 관점

  • 특정 고객에 종속되지 않게 확장 가능하고 비용 효율적인 방식으로 설계하는 것 

3. 개발자 경험

  • 내부 개발자들을 위한 개발자 경험도 중요한 설계 요소 (예: API 문서화)

 

1장 - API의 설계에 가장 작은 단위가 되는 리소스. 리소스는 어떻게 설계해야 하는 것일까?

리소스가 어떤 방식으로 저장되고 (데이터 베이스 모델이 아니다.) 어떤 객체로 표시되는지 상관없이()
네트워크를 통한 상호작용에만 중점을 둔다. 

"데이터 베이스 모델이 아니다"라는 의미가 무엇인지 좀 더 살펴보자. 
즉 시간에 따라 데이터베이스의 스키마라던지, 저장 방식 등이 변경될 수 있기에
데이터 모델이 변경될때마다 API의 설계 역시 동일하게 변경된다면 이는 취약한 API라는 것이다. 
API 설계는 데이터베이스의 기술 동향과 무관하게 독립적으로 발전시켜야한다.

즉, API 사용자의 입장에서 리소스의 실제 상세 내용과 내부 코드를 이해할 필요는 없다.

데이터 베이스 모델 뿐만 아니라, 객체지향 프로그램에서의 객체 및 도메인 모델, 동작을

직접적으로 노출하기 보단 API 사용 경계의 외부에서 고려하는 것이 중요하다. 

1장 - 작은 단위를 알았으니, 웹 API의 기본 방식에 대해 알아보자 - 메시지 기반
또한 웹 API는 메시지 기반이다. 요청 메시지를 서버에 송신하고 응답 메시지를 수신한다. 
원하는 결과를 도출하기 위해 시스템 간 대화 방식의 메시지 교환을 고려하고, 요구사항이
변할 때 대화가 어떻게 발전하는지도 고려한다. 

 

1장 - 기본 방식에 더 나아가, 웹 API 설계 원칙 5가지

원칙 1 : 협업을 통한 설계하기 (2장) 
원칙 2 : 결과에서 출발하기 (3 - 6장) -> 중점으로 보기
원칙 3 : 필요에 의한 API 설계 요소들을 선택하기 (7 - 12장) 
원칙 4 : API 문서의 중요성 (13장)
원칙 5 : 변화에 대응하는 API (14장)

 

며칠전에 작성하다 말았던 테스트 코드를 다시 작성해보았는데, 생각이 안나서 (?) 계속 예제를 참고하며 코딩했다. 
이번에 새로 깨달은 점은 @MockBean을 하는 경우이다. 임시 객체만 생성하고 원하는 값을 주입하고자 할 때 사용하는 것이구나 싶다. 그리고 테스트 케이스도 정석으로 잘되는 동작, 정석이 아닌 경우에도 잘 되는 동작, 에러나는 동작 등 코드의 전체 브랜치를 확인할 수 있게 작성해야 하는 것도 배웠다. 내일은 다른 Controller에 대해 훝어보고, 서비스 코드에서의 테스트 코드는 어떻게 짜는 것인지 공부해보도록 하자. 

package org.springframework.samples.petclinic.owner;

import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDate;


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

@WebMvcTest(OwnerController.class)
public class OwnerControllerTests2 {
	@Autowired
	MockMvc mockMvc;

	@MockBean
	OwnerRepository owners;

	private static final int TEST_OWNER_ID = 1;

	private Owner george() {
		Owner george = new Owner();
		george.setId(TEST_OWNER_ID);
		george.setFirstName("George");
		george.setLastName("Franklin");
		george.setAddress("110 W. Liberty St.");
		george.setCity("Madison");
		george.setTelephone("6085551023");
		Pet max = new Pet();
		PetType dog = new PetType();
		dog.setName("dog");
		max.setType(dog);
		max.setName("Max");
		max.setBirthDate(LocalDate.now());
		george.addPet(max);
		max.setId(1);
		return george;
	};

	@BeforeEach
	void setup() {
		given(this.owners.findByLastName(eq("George"), any(Pageable.class)))
			.willReturn(new PageImpl<Owner>(Lists.newArrayList(george())));

		given(this.owners.findByLastName(eq("Franklin"), any(Pageable.class)))
			.willReturn(new PageImpl<Owner>(Lists.newArrayList(george())));

		given(owners.findByFullName(any(), any(), any(Pageable.class))).willReturn(
			new PageImpl<Owner>(Lists.newArrayList(george()))
		);
		given(owners.findById(TEST_OWNER_ID)).willReturn(
			george()
		);
	}

	@Test
	void testGetOwnerSuccess() throws Exception {
		mockMvc.perform(
			get("/owners")
		).andExpect(status().is3xxRedirection())
			.andExpect(view().name("redirect:/owners/1"));
	}

	@Test
	void testGetOwnersSuccess() throws Exception {
		Mockito.when(this.owners.findByFullName(any(), any(), any(Pageable.class))).thenReturn(
			new PageImpl<Owner>(Lists.newArrayList(george(), george()))
		);

		mockMvc.perform(
			get("/owners")
		).andExpect(status().isOk())
			.andExpect(view().name("owners/ownersList"));
	}
}

호기롭게 테스트코드 작성하는 것에 도전했으나, 모르는 것이 많아 제대로 진행을 할 수 없었다..
모르는 것을 정리부터 해 보았는데 다음과 같다.

1. 먼저 파라미터에는 어떤 값을 어떻게 전달해줘야 하는 것인가?
특히 스프링 내부적으로 전달되는 것 같아보이는
BindingResult나 Model같은 건..? 
2. DB를 통해 interface의 형으로 전달되는 데이터를 어떻게 객체로 만들 수 있는 것일까?
3. Controller에서 리턴하는 문자열만 검증하면 검증이 완료되었다는 것일까?

1번과 2번은 왠지 @Autowired와 연관되어 보이긴 해서 다음과 같이 작성해 주었는데

문제는 컨트롤러에서 private하게 호출하는 메서드가 리턴하는 값을 어떤식으로 만들어서 제공하는지다. 

package org.springframework.samples.petclinic.owner;

import org.junit.jupiter.api.Test;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.Repository;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

import java.beans.PropertyEditor;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.BDDAssumptions.given;
import static org.mockito.Mockito.when;

public class JYOwnerController {

	@Autowired
	OwnerController ownerController;

	@Autowired
	OwnerRepository owners;

	@Autowired
	BindingResult result;

	@Autowired
	Model model;

	@MockBean
	Page<Owner> ownersResults;

	@Test
	public void processFindFormTest() {
		String firstname = "";
		String lastname = "";

		int page = 1;
		Owner owner = new Owner();
		owner.setLastName("");
		owner.setFirstName("");

		given(
			owners.findByFullName(
				firstname,
				lastname,
				PageRequest.of(page - 1, 5)
			)
		).willReturn(ownersResults);

		ownerController.processFindForm(page, owner, result, model);
	}
}



spring 개발자가 작성한 코드를 확인해보면,
먼저 테스트 코드가 총 3개로 나누어져 있는 것을 확인할 수 있다. 

나의 경우에는 NoOwnersFound는 생각도 못했던 것 같다. 


또한 예제 코드를 보니깐, db 객체를  더미 객체로 주입 받고, 미리 어떤 값을 셋팅하도록 되어 있었다. (Mockito.when, then Return) 또한 실제 컨트롤러 객체에 어떤 작업을 하는 것이 아니라, mockMvc를 통해, peform().andExpect().andExpect()를 수행하더라.

private static final int TEST_OWNER_ID = 1;

@Autowired
private MockMvc mockMvc;

@MockBean
private OwnerRepository owners;

@Autowired
ApplicationContext applicationContext;

@Test
	void testProcessFindFormSuccess() throws Exception {
		Page<Owner> tasks = new PageImpl<Owner>(Lists.newArrayList(george(), new Owner()));
		Mockito.when(this.owners.findByLastName(anyString(), any(Pageable.class))).thenReturn(tasks);
		mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList"));
	}

 
한가지 궁금한건, Page, PageImpl 인데, 이것은 나중에 찾아보자.

'Development > Test' 카테고리의 다른 글

Junit 5 - 1. 기본 개념 및 실행 방식에 대해서  (0) 2024.01.22

스프링 레거시 프로젝트 기반 전시 정보 데이터를 다양한 곳에서 가져와 데이터베이스에 저장하는 프로그램을 작성한 적이 있다. 스프링 부트도 공부할 겸 하여 부트로 전환하면서 새로운 자바 문법 (17)에 맞게 개발을 진행하려고 한다. 
그 전에 서비스의 기능 및 이에 필요한 api 설계 및 앞으로의 계획을 세워보고자 한다.

수집 서비스의 기능
1. 매일 정해진 시간에 자동으로 A, B, C 에서 현재 날짜 기준 전시 중인 정보를 수집해 database에 overwrite하여 저장한다.
2. 전시 정보 수집 도중 실패할 시 이전 데이터로 다시 원복하고, 그 후 1회 정도  (5분 간격) 전시 정보 수집을 재시도 한다.
3. 수집이 끝나면 해당 상태에 대해 slack 또는 email로 수집 완료 알림이 간다. 
(2번과 3번은 1번이 끝나면 시도해봐야겠다.)


필요한 api
먼저 1번의 기능에 필요한 api는 다음과 같다. 그런데 이런 식으로 api를 작성해도 되는 것일까? 
이름의 명칭이라던가 parameter로 오는 것이 하나라 뒤에 붙여주긴 했는데 parameter가 계속 추가되면 뒤에 계속해서 붙여주면 되는 것일까? 싶다. restapi 작성법은 개발하며 다른 프로젝트를 참고하면서 수정을 해 봐야 할 것 같다.

/collectExhibition?service=leeum
/collectExhibition?service=open&type=json



일정
최소 하루 30분을 목표로 일주일 동안 기능 1부터 완성해보려고 한다.

0317 ~ 0323
17, 18 : 프로젝트 셋팅 및 folder 구조 구성 및 api 작성하기 1 (그동안 배웠던 것 : annotation 적용해보기 등)
19 : api 작성하기 2 (병렬적으로 데이터를 긁어와 db에 저장하는 구조 찾아보면서 작성해보기)
20 : api 작성하기 3 
21 : api 작성하기 4 (데이터 베이스는 h2 메모리 데이터 베이스 사용해보기)
22 : 테스트 코드 작성하기 1
23 : 테스트 코드 작성 및 기능 1완료 하기, 2번에 대한 계획 세우기

0324 ~ 0330 : 
2번 기능 진행 ...

0331 ~ 0406: 
3번 기능 진행 ...

'Development > Spring' 카테고리의 다른 글

[Spring-petclinic] 엉망진창 test code 작성하기 - 2  (0) 2024.03.28
JPA 개념 이해  (0) 2023.10.31

IO중에서 객체의 직렬화에 관심이 있어 이 부분을 위주로 공부를 했다. 
먼저 직렬화가 어떤 개념인 것인지 잠깐 보고 지나가보자.

직렬화-serialize란 자바의 객체를 바이트로 변환 후에 database나 file, memory에 저장하는 개념이다.
이와 반대로 database나 file, memory로부터 바이트를 읽어와 자바 객체를 만드는 것을 역직렬화-deserialize라고 한다.

https://data-flair.training/blogs/serialization-and-deserialization-in-java/

직렬화를 실무에선 어떻게 활용하고 있을까? 
많이 사용하고 있는 Jackson 라이브러리에서 이미 사용이 되고 있다. (json을 외부 서버로부터 받고 객체화 하는 부분)
본인의 토이프로젝트에서도 이미 사용이 되고 있었다. (역직렬화)
아마 database저장할 때에도 많이 사용이 될 것 같은데 자세한 건 잘 모르겠다.

public void scrap() throws Exception {
		logger.info("process start.");
		
		String body = WebClient.create().get()
							  .uri(createUriComponent())
							  .accept(MediaType.APPLICATION_JSON)
							  .retrieve()
							  .bodyToMono(String.class)
							  .block();
		
		if (!body.isEmpty()) {
			try {
				JsonNode jsonNodeRoot = new ObjectMapper().readTree(body);
				if (jsonNodeRoot.get("list") != null) {
			    	String jsonItemRoot = jsonNodeRoot.get("list").toString();
			    	List<LeeumData> listItem = new ObjectMapper().readValue(jsonItemRoot, new TypeReference<List<LeeumData>>() {});
					
					for (LeeumData list : listItem) {
						ExhibitionDto data = new ExhibitionDto("leeum", 
																	list.getTitle(), 
																	list.getImage(),
																	Formatter.parse(list.getStartDate()),
																	Formatter.parse(list.getEndDate()));
						exhibitionList.add(data);
					}
		        }
			} catch (JsonProcessingException | ParseException | 
					HttpClientErrorException| HttpServerErrorException e) {
				throw new Exception(e.getMessage());
			}
		}
		
		logger.info("process end.");
		resultListener.executed();
	}


간단하게 jackson 관련한 코드를 작성해보았는데 직접 객체에 java.io.Serializable을 하지 않아도 되었다.
그러나 기본적으로 데이터를 쓸때 필요한 getter method가 있어야 에러가 나지 않았다.

package com.scraping.app.serialize;

import java.io.File;
import java.io.IOException;

import com.fasterxml.jackson.core.exc.StreamWriteException;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Serial {
	public static void main(String[] args) throws StreamWriteException, DatabindException, IOException {
		UserInfo userInput = new UserInfo("name", "password");
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.writeValue(new File("/Users/user/workspace/javaStudy/log/result.txt"), userInput);
	}
}
package com.scraping.app.serialize;

public class UserInfo {
	String password;
	String name;
	
	public UserInfo(String password, String name) {
		this.password = password;
		this.name = name;
	}
	
	public String getPassword() {
		return password;
	}
//	public void setPassword(String password) {
//		this.password = password;
//	}
	public String getName() {
		return name;
	}
//	public void setName(String name) {
//		this.name = name;
//	}
}


여기서 조금 더 나아가 custom한,
예를 들자면 json으로 보내기 전 특정 필드값을 추가하고 싶다면 어떻게 해야 할까?
커스텀하게 serializerProvider를 만들어주면된다.

public class Serial {
	public static void main(String[] args) throws StreamWriteException, DatabindException, IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule module = new SimpleModule("CustomStickerSrializer", new Version(1, 0, 0, null, null, null));
        module.addSerializer(Sticker.class, new CustomStickerSerializer());
        objectMapper.registerModule(module);

        Sticker sticker = new Sticker("paper", "cat");
        String stickerJson = objectMapper.writeValueAsString(sticker);

        System.out.println(stickerJson);
    }
}
package com.scraping.app.validation;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

public class CustomStickerSerializer extends StdSerializer<Sticker>{
	public CustomStickerSerializer() {
		this(null);
	}
	public CustomStickerSerializer(Class<Sticker> t) {
		super(t);
	}

	@Override
	public void serialize(Sticker value, JsonGenerator gen, SerializerProvider provider) throws IOException {
		gen.writeStartObject();
		gen.writeStringField("type", value.getType());
		gen.writeStringField("char", value.getCharacter());
		gen.writeStringField("brand", "A");
	}

	
}

 

결과는 다음과 같이 나온다.
{"type":"paper","char":"cat","brand":"A"}



참고
https://www.baeldung.com/jackson-object-mapper-tutorial

https://koocci-dev.tistory.com/25

먼저 애노테이션을 왜 사용하는 것일까? 이에 좋은 예제가 있어 작성해보았다.


예를 들어 클래스의 메서드의 갯수 및 에러없이 잘 동작하는지에 대한 확인을 하는 테스트 코드를 작성해본다고 하자. 

다음은 방금 언급한 테스트 메서드의 내용을 확인하는 클래스이다.

import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Set;

public class TestRunnerTest {

    public void singleMethodTest() {
        TestRunner runner = new TestRunner(SingleMethodTest.class);
        Set<Method> testMethods = runner.getTestMethods();
        if (1 == testMethods.size())
            System.out.println("expected single test method");

        Iterator<Method> it = testMethods.iterator();
        Method method = it.next();

        final String testMethodName = "testA";
        if (testMethodName.equals(method.getName()))
            System.out.println("expected " + testMethodName + " as test method");

        runner.run();
        if (1 == runner.passed())
            System.out.println("expected 1 pass");

        if (0 == runner.failed())
            System.out.println("expected no failures");

    }

    public void multipleMethodTest() {
        TestRunner runner = new TestRunner(MultipleMethodTest.class);
        Set<Method> testMethods = runner.getTestMethods();
        if (2 == testMethods.size())
            System.out.println("expected multiple test method");

        Iterator<Method> it = testMethods.iterator();
        Method method = it.next();

        final String testMethodNameA = "testA";
        final String testMethodNameB = "testB";
        if (testMethodNameA.equals(method.getName()))
            System.out.println("expected " + testMethodNameA + " as test method");

        if (testMethodNameA.equals(method.getName()))
            System.out.println("expected " + testMethodNameB + " as test method");

        runner.run();
        if (2 == runner.passed())
            System.out.println("expected 2 pass");

        if (0 == runner.failed())
            System.out.println("expected no failures");

    }
}

class SingleMethodTest {
    public void testA() {}
}

class MultipleMethodTest {
    public void testA() {}
    public void testB() {}
}

 


다음 코드는 테스트 클래스를 수행하는 환경 및 각 클래스의 메서드 정보를 제공하는 클래스이다.

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class TestRunner {
    private Class testClass;
    private int failed = 0;
    private Set<Method> testMethods = null;

    public static void main(String[] args) throws Exception {
        TestRunner runner = new TestRunner(args[0]);
        runner.run();
        System.out.println(
                "passed: " + runner.passed() + " failed: " + runner.failed
        );
        if(runner.failed() > 0)
            System.exit(1);
    }

    public TestRunner (Class testClass){
        this.testClass = testClass;
    }

    public TestRunner(String className) throws Exception {
        this(Class.forName(className));
    }

    public Set<Method> getTestMethods(){
        if (testMethods == null)
           loadTestMethods();
        return testMethods;
    }

    private void loadTestMethods() {
        testMethods = new HashSet<Method>();
        for (Method method : testClass.getDeclaredMethods()){
            testMethods.add(method);
        }
    }

    public void run(){
        for (Method method: getTestMethods()) {
            run(method);
        }
    }

    private void run(Method method) {
        try {
            Object testObject = testClass.newInstance();
            method.invoke(testObject, new Object[] {});
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
            Throwable cause = e.getCause();
            if (cause instanceof  AssertionError)
                System.out.println(cause.getMessage());
            else
                e.printStackTrace();
            failed++;
        }
    }

    public int passed() {
        return testMethods.size() - failed;
    }

    public int failed() {
        return failed;
    }

}

 

수행시 다음과 같은 옵션을 주면 된다.

 


하지만... 
TestRunnerTest 클래스의 경우 singleMethodTest 와 multipleMethodTest에 비슷한 내용이 많이 보인다. 
이 경우 중복 코드를 바깥으로 빼낼 수 있을 것 같다. 동시에 이 메서드들은 내가 확인하고 싶은 메서드가 아닐 때 어떻게 해야할까?

즉, 내가 표시한 메서드만 테스트 메서드로 인정하고 싶다. 이런 경우 어떻게 해야할까? 바로 애너테이션을 이용하면 된다. 

public @interface TestMethod {}

 

그리고 TestRunnerTest 클래스는 다음과 같이 수정해준다.

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class TestRunnerTest {
    private TestRunner runner;
    private static final String methodNameA = "testA";
    private static final String methodNameB = "testB";

    private void runTests (Class testClass) {
        runner = new TestRunner(testClass);
        runner.run();
    }

    private void verifyTests (String... expectedTestMethodNames) {
        verifyNumberOfTests(expectedTestMethodNames);
        verifyMethodNames(expectedTestMethodNames);
        verifyCounts(expectedTestMethodNames);
    }

    private void verifyCounts(String... testMethodNames){
        if (testMethodNames.length == runner.passed())
            System.out.println("expected 1 pass");

        if (0 == runner.failed())
            System.out.println("expected no failures");
    }

    private void verifyNumberOfTests(String... testMethodNames) {
        if (1 == testMethodNames.length)
            System.out.println("expected " + testMethodNames.length + " test method(s)");
    }

    private void verifyMethodNames(String... testMethodNames) {
        Set<String> actualMethodNames = getTestMethodNames();
        for (String methodName : testMethodNames) {
            if (actualMethodNames.contains(methodName)) {
                System.out.println("expected " + methodName + " as test method");
            }
        }
    }

    private Set<String> getTestMethodNames() {
        Set<String> methodNames = new HashSet<String>();
        for (Method method: runner.getTestMethods()){
            methodNames.add(method.getName());
        }
        return methodNames;
    }

    @TestMethod
    public void singleMethodTest(){
        runTests(SingleMethodTest.class);
        verifyTests(methodNameA);
    }

    @TestMethod
    public void multipleMethodTest(){
        runTests(MultipleMethodTest.class);
        verifyTests(methodNameA, methodNameB);
    }
}

class SingleMethodTest {
    @TestMethod public void testA() {}
}

class MultipleMethodTest {
    @TestMethod public void testA() {}
    @TestMethod public void testB() {}
}

 

 

그러나 실행을 하면 다음과 같이 에러가 뜬다. private 메서드로 정의된 함수를 호출하면서 에러가 발생되는 것인데 
이 경우 어떻게 해 줘야할까? 


바로 애노테이션 함수인 것만 테스트 메서드로 추가하면 된다. 

private void loadTestMethods() {
        testMethods = new HashSet<Method>();
        for(Method method : testClass.getDeclaredMethods()){
            if(method.isAnnotationPresent(TestMethod.class)){
                testMethods.add(method);
            }
        }
    }

 

 

에러가 발생하진 않긴 하지만 수행이 하나도 되지 않는다. 그 이유는 무엇일까?

 

바로 애노테이션의 유지 (Retention)과도 관련이 있다.
기본적으로 실행시 애노테이션 정보를 얻을 수 없다. 그렇기 때문에 실행시에도 애노테이션 정보를 유지해서 해당 메서드의 정보를 받아올 수 있도록 Retention을 설정해야 하는 것이다.

애노테이션을 다음과 같이 수정해준다.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface TestMethod {
}

 

잘 작동되는 것을 확인할 수 있다!

 

추가적으로 애노테이션 형식이 붙을 수 있는 타입을 지정할 수 있다. 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestMethod {
}


즉 이 경우 클래스 앞에 애노테이션이 붙으면 에러가 나는 것을 확인할 수 있다.

 

애노테이션 활용
youtube.com/watch?v=P5sAaFY3O2w

https://techblog.woowahan.com/2684/

https://devonce.tistory.com/42

https://mangkyu.tistory.com/174
Custom한 validator를 적용해보려 했으나 실패하였다.

package com.scraping.app.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import shaded_package.javax.validation.Constraint;
import shaded_package.javax.validation.Payload;


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UrlValidator.class)
public @interface ValidationUrl {
	public String message() default "Invalid url format";
    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
package com.scraping.app.validation;

import java.util.regex.Pattern;

import org.springframework.web.bind.MethodArgumentNotValidException;

import shaded_package.javax.validation.*;
import shaded_package.javax.validation.constraintvalidation.*;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class UrlValidator implements ConstraintValidator<ValidationUrl, String[]>{
	private static final String pattern = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]";
	
	@Override
	public boolean isValid(String[] urls, ConstraintValidatorContext arg1) {
		throw new IllegalArgumentException("Illegal");
		//System.out.println("isValid is called..");
		//return Pattern.matches(pattern, urls[0]);
	}

}

 

public void setURL(@ValidationUrl String newURL) {
		this.URL = newURL;
	}
    
    ...
    

test code
@Override
public void inValidUrlTest() throws Exception{
    openApiCollector.setURL("://127.0.0.1:1080");
    //exceptionRule.expect(MethodArgumentNotValidException.class);
    exceptionRule.expect(IllegalArgumentException.class);
}


애노테이션의 리텐션 타입과 메모리를 비교하며 이해하기
소스 코드  (Source) -> 바이트코드 (Class) -> 클래스 로딩 in JVM (Runtime)

/**
 * Annotation retention policy.  The constants of this enumerated type
 * describe the various policies for retaining annotations.  They are used
 * in conjunction with the {@link Retention} meta-annotation type to specify
 * how long annotations are to be retained.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}



참고 : 제프 랭어 - 자바프로그래밍 / 교학사 

멀티쓰레드 프로그래밍을 왜 하는걸까?
하나의 프로그램에서 (프로세서) 동시에 코드가 수행되어야 하는 경우 Thread를 여러개 사용하는 멀티쓰레드 프로그래밍을 할 수 있다. (예: 채팅 프로그램 - 채팅창에 텍스트 내용을 입력하면서 동시에 다른 사람의 챗팅 내용을 읽어와야 하는 경우) 또한 자원을 효율적으로 사용하면서 (하나의 객체를 공유해 사용) 다양한 작업을 수행하기 위해 멀티쓰레드 프로그래밍을 한다.

Thread의 동작방식에 대해 알아보자!

동시성의 경우 각 쓰레드가 짧은 시간동안 번갈아 수행된다.



 
Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하여 Thread를 생성할 수 있다.


Core와의 관계
Core 1개당 하나의 작업만 수행할 수 있다. Core가 4개가 있으면 4개의 작업을 동시에 수행할 수 있다. 
아주 짧은 시간 동안 각 쓰레드(작업)을 번갈아 가면서 수행한다. (Core의 갯수보다 Thread의 갯수가 2개 이상 많은 경우)



싱글쓰레드 VS 멀티쓰레드 

싱글쓰레드(작업 A,B를 하나의 쓰레드에) vs 멀티쓰레드의 차이(작업 A, B를 각각의 쓰레드에)
단일 코어에서 Thread가 2개 있다면?  -> 

public class MultiThread {
	
	public static void main(String[] args) {
		
		long start = System.currentTimeMillis();
		for (int i=0; i<500; i++) {
			System.out.print("|");
		}
		long current = System.currentTimeMillis();
		System.out.println("총 수행 시간 " + (current - start));
		
		
		for(int i=0; i<500; i++) {
			System.out.print("-");
		}
		current = System.currentTimeMillis();
		System.out.println("총 수행 시간 " + (current - start));
		
	}
	
}

단일 코어의 경우 한번에 한 작업만 수행할 수 있기에 동시에 자원을 공유해서 생기는 문제가 없다.
(그러나 하나의 작업이 끝나야 다음 작업을 수행할 수 있다.)


코어에서 Thread를 2개 수행하는 경우?

public class MultiThread {
	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		
		Thread th = new Thread(new Runnable() {
			public void run() {
				for(int i=0; i<500; i++) {
					System.out.print("-");
				}
				long current = System.currentTimeMillis();
				System.out.println("총 수행 시간 " + (current - start));
			}
		});
		th.start();
		
		for (int i=0; i<500; i++) {
			System.out.print("|");
		}
		long current = System.currentTimeMillis();
		System.out.println("총 수행 시간 " + (current - start));
	}
}

 

쓰레드의 수행 순서나 수행 시간은 장담할 수 없다. (Thread Scheculer가 모든 것을 결정한다.)
코어에 할당된 쓰레드가 동시에 수행될 수 있다. (위의 예의 경우 콘솔창에 프린트 하려는 경우) 


start() 메서드의 의미 


+ heap 메모리를 공유한다


 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Search {
	private URL url;
	private String searchString;
	private Pattern pattern;
	private int matches;
	
	public String getUrl() {
		return this.url.toString();
	}

	public String getSearchString() {
		return this.searchString;
	}
	
	public int getMatches() {
		return this.matches;
	}

	public Search (String url, String searchWord) {
		try {
			this.url = new URL(url);
			this.searchString = searchWord;
			this.pattern = Pattern.compile(searchWord, Pattern.CASE_INSENSITIVE);
		} catch (MalformedURLException e) {
			e.printStackTrace();
		}
	}
	
	public void searchString() throws IOException {
		InputStream input = url.openConnection().getInputStream();
		BufferedReader reader = new BufferedReader(new InputStreamReader(input));
		
		String line;
		int result = 0;
		while ((line = reader.readLine()) != null) {
			Matcher matcher = pattern.matcher(line);
			while (matcher.find()) {
				result++;
			}
		}
		
		matches = result;
	}
}
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class Server extends Thread{

	private List<Search> searchList;
	private ResultListener listener;
	
	public Server(ResultListener listener) {
		searchList = Collections.synchronizedList(new LinkedList<Search>());
		this.listener = listener;
		start();
	}
	
	public void add(Search search) {
		this.searchList.add(search);
	}
	
	public void run() {
		while(true) {
			try {
				if(searchList.size() > 0) {
					Search search = searchList.get(0);
					executed(search);
				}
				Thread.yield();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	private void executed(Search search) throws IOException {
		search.searchString();
		listener.executed();
	}
	
}
public interface ResultListener {
	public void executed();
}

 

import static org.junit.Assert.assertTrue;

import java.io.IOException;

import org.junit.jupiter.api.Test;

public class ServerTest {

	private int numbefOfResults;

	class Result implements ResultListener {
		@Override
		public void executed() {
			numbefOfResults++;
		}
	}
	
	private final String url = "http://www.langrsoft.com/";
	private final String searchString = "langr";
	private final String[] URLS = new String[] {
		url,
		url,
		url
	};
	
	@Test
	void testSearch() throws IOException {
		Server server = new Server(new Result());
		
		for (String url : URLS) {
			server.add(new Search(url, searchString));
		}
		
		assertTrue(waitForResults());
	}
	
	private boolean waitForResults() {
		long start = System.currentTimeMillis();
		while (numbefOfResults < URLS.length) {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			if (System.currentTimeMillis() - start > 3000) {
				return false;
			}
		}
		return true;
	}
	
}

 

쓰레드의 상태
NEW (쓰레드가 처음 생성되었고, 아직 실행 대기 중인 상태)
RUNNABLE (실행중인 상태)
TERMINATED (종료 상태)
BLOCKED (I/O 대기 상태)
TIME WAITING (특정 시간 동안 대기 상태)
WAITING (대기 상태)




쓰레드의 우선순위
우선순위의 범위는 1 ~ 10이 있다. 기본으로는 5가 부여된다.
우선순위가 높을수록 할당하는 시간, 순서를  높게 부여된다고 한다.
그러나 운영체제별로 우선순위에 대한 정책이 다를 수 있으니 우선순위가 높다고 하더라도
설정 여부에 대한 예측이 어렵다. 

public class ThreadPriority {
	public static void main(String[] args) {
		Thread th1 = new Thread(new Runnable() {
			public void run() {
				for(int i=0; i<300; i++) {
					System.out.print("|");
					for(int x=0; x<10000000; x++);
				}
			}
		});
		
		Thread th2 = new Thread(new Runnable() {
			public void run() {
				for(int i=0; i<300; i++) {
					System.out.print("-");
					for(int x=0; x<10000000; x++);
				}
			}
		});
		th2.setPriority(7);

		th1.start();
		th2.start();
	}
}

위의 경우도 실행시간이 높은 경우는 단 한1번 뿐이었다. (4코어에서 테스트)




 

 

 

쓰레드에서 자원을 다루는 법 with synchronized

문제가 생기는 코드 

class Bank {
	private int money = 10000;
	
	int getMoney() {
		return this.money;
	}
	
	void withdraw(int amount) {
		money -= amount;
	}
}

public class MyThread {

	public static void main(String[] args) {
		Bank myBank = new Bank();
		
		Thread mobile = new Thread(new Runnable() {
			public void run() {
				for(int i=0; i<15; i++) {
					if (myBank.getMoney() > 0) {
						try {
							Thread.sleep(2);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						myBank.withdraw(1000);
						System.out.println("mobile withdraw success! money : " + myBank.getMoney());
					}
				}
			}
		});
		
		Thread atm = new Thread(new Runnable() {
			public void run() {
				for(int i=0; i<15; i++) {
					if (myBank.getMoney() > 0) {
						try {
							Thread.sleep(2);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						myBank.withdraw(1000);
						System.out.println("atm withdraw success! money : " + myBank.getMoney());
					}
				}
			}
		});
		
		mobile.start();
		atm.start();
		
		while (myBank.getMoney() > 0) {
			Thread.yield();
		}
		
		System.out.println("current money = " + myBank.getMoney());
	}
}

Synchronized와 락?

 

 

리팩토링 및 문제 해결 코드

class Bank {
	private int money = 10000;
	
	int getMoney() {
		return this.money;
	}
	
	void withdraw(int amount) {
		money -= amount;
	}
	
	synchronized void run() {
		for(int i=0; i<15; i++) {
			if (getMoney() > 0) {
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				withdraw(1000);
				System.out.println("atm withdraw success! money : " + getMoney());
			}
			if (getMoney() < 0) {
				System.out.println("current money : " + getMoney());
			}
		}
	}
}

public class MyThread {
	public static void main(String[] args) {
		Bank myBank = new Bank();
		
		Thread mobile = new Thread(new Runnable() {
			public void run() {
				myBank.run();
			}
		});
		
		Thread atm = new Thread(new Runnable() {
			public void run() {
				myBank.run();
			}
		});
		
		mobile.start();
		atm.start();
	}
}

 

synchronized 관련 재미있는 예제 

import java.util.Date;

public class Clock implements Runnable{
	private Listener listener;
	private boolean run = true;
	
	public Clock(Listener listener) {
		this.listener = listener;
		new Thread(this).start();
	}

	public void stop() {
		run = false;
	}
	
	public void run() {
		long lastTime = System.currentTimeMillis();
		
		while (run) {
			try {
				long now = System.currentTimeMillis();
				Thread.sleep(10);
				while (System.currentTimeMillis()/1000 - lastTime/1000 >= 1) {
					listener.update(new Date());
					lastTime = System.currentTimeMillis();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

 

import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import org.junit.jupiter.api.Test;

class ClockTest {
	Clock clock;
	Object monitor = new Object();
	private final int seconds = 5;
	
	@Test
	void testClock() {
		List<Date> tics = new ArrayList<Date>();
		
		clock = new Clock(new Listener() {
			int count = 0;
			
			public void update(Date date) {
				tics.add(date);
				if (++count == seconds) {
					synchronized(monitor) {
						monitor.notifyAll();
					}
				}
			}
		});
		
		try {
			synchronized(monitor) {
				monitor.wait();
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		clock.stop();
		verify(tics, seconds);
	}
	
	void verify(List<Date> tics, int seconds) {
		assertEquals(tics.size(), seconds);
		for (int i=1; i<seconds; i++) {
			assertEquals(1, getSeconds(tics, i));
		}
	}
	
	int getSeconds(List<Date> tics, int index) {
		Calendar cal = new GregorianCalendar();
		cal.setTime(tics.get(index));
		int now = cal.get(Calendar.SECOND);
		cal.setTime(tics.get(index-1));
		int then = cal.get(Calendar.SECOND);
		if (then == 0) {
			then = 60;
		}
		
		System.out.println("time = " + (now - then));
		return now - then;
	}
}

 

import java.util.Date;

public class Listener {
	public void update(Date date) {
		
	}
}

 

 

 

 

 

 

 




동기화
synchronized 메서드 혹은 Block으로 시작하는 이름의 자료구조로 설정할 수 있다.
데이터를 다른 쓰레드에서 동시에 쓰지 못하도록 메서드를 한 쓰레드에서만 선점하는 역할을 한다.
락을 건다고 한다.

데드락 
교착 상태 

Main 쓰레드
Main 쓰레드의 경우 call stack에 가장 하위, 즉 맨 처음 호출되는 것이 아닐까..?

'Development > Java' 카테고리의 다른 글

[white ship - 12주차] IO  (1) 2024.03.16
[white ship - 11주차] 애노테이션  (0) 2024.03.09
[white ship - 8주차] 인터페이스  (0) 2024.02.14
[white ship - 7주차] 패키지  (0) 2024.02.14
[white ship - 6주차] 상속  (0) 2024.02.06

+ Recent posts