API 호출시 겪었던 일에 대한 정리


환경

  • API 호출 경험


API 호출시 겪었던 일

기존에 잘 작동하던 코드가 있었다. API 호출한 결과를 가져와서 필터링을 통해 원하는 조건의 값들만을 가져오는 코드였다. 그런데 잘 작동하던 코드가 갑자기 안되는 경우가 생겼다… 살펴보니 분명히 웹에서 검색했을때는 우리가 원하는 값들이 있었는데 필터링 했을때는 해당하는 값들이 없다는거였다. 한참을 살펴봤지만 코드에는 문제가 없는거 같았다. 하지만 역시 제대로 보니 코드에 문제가 있었었다. 역시 코드가 잘못이 아니라 작성한 사람의 잘못이었다!

다른분의 도움으로 발견한 부분이었는데 문제의 해결은 필터링 하기전에 값들이 딱 100과 같이 5나 10의 배수로 깔끔하게 나오는거에서 영감을 받았다. 알고보니 코드에서는 API를 통해 전체 값들을 가져오고 그걸 필터링하는 과정으로 원하는 값을 가져오는데 호출시 가져오는 아이템 수가 정해져있어서 원하는 값들이 웹상에 있어도 API 요청으로 오지 않는거였다. 즉, API 호출시 최대 아이템 수가 제한이 있었던게 문제였다.

예를 들면 원하는 값이 101번째에 있는데 100번째까지만 응답이 와서 필터링해도 원하는 101번째 값이 나오지 않는거다.

결국 API 호출시 조건을 최대한 걸어서 원하는 값들만 가져오고 최대 아이템 수가 온다면 다음 값들을 가져오는 방식으로 코드를 수정해서 문제를 해결할 수 있었다.

여기서 배운점이 3가지 정도가 있다.

  1. API 호출시 전체를 가져와서 필터링하기보다 조건을 넣을 수 있다면 API 호출시 미리 넣는게 요청자쪽에서의 부담과 실수를 줄일 수 있다.
  2. API 호출시 최대 응답 아이템 수가 정해져 있는 경우가 많으니 이를 주의하자.
  3. 문제를 함께 해결하면 아이디어가 생겨서 도움이 될 수 있다.

어찌보면 당연한 이야기지만 해당 부분으로 시간을 좀 소요해서 정리를 해봤다.

다음부터는 실수하지 말자!


참고자료

Retrospective about what happened when calling API


Environment and Prerequisite

  • API Call Experience


What I experienced when calling API

There was a code worked well. It was a code that filtering what we wanted after get the result of API. However one day it did not work in one of our services. After take a looking it, we found that there were values what we wanted on web but those values were not exist after filtering. I looked into it long time but I thought there was no problem. However there was a problem with the code. It wasn’t the code’s fault, it was developer’s fault!

With help of my colleague, we inspired by API response result item size before filtering. Its size was a multiple of 5 or 10. We found that there was a limit of API response item size. That was why we could not get the values what we wanted. In my code, we received the response items and filtered it but there was a max limit on item size. So we could not get what we wanted. Problem was max item size in API call.

Let’s suppose expected value is the 101st but API result contain only until 100th value so we cannot get the expected 101st value.

At last we add conditions to API call and add pagination code to API call to solve above issue.

I learned three things.

  1. To minimize burden and mistake of API caller, add conditions to API call rather than get all response items.
  2. Be careful when use API call because there can be a limit of response item size.
  3. Solving problems together can make solving issue faster.

It was a simple thing but I took some time to solve it. So I wrote a post about it.

Be careful next time!


Reference

rsync에서 22번 포트가 아닌 다른 포트를 사용해보자


환경

  • Linux
  • Bash shell(/bin/bash)
  • rsync


rsync

방법

  • -e 옵션을 사용
  • 아래 예시에서는 모두 2222번 포트를 사용하도록 작성함
-e 'ssh -p 2222'


IP주소 혹은 도메인을 입력하는 경우

  • 형태
rsync -e 'ssh -p 2222' [target file or directory] [user]@[IP address or domain]:[destination]
  • 예시
rsync -e 'ssh -p 2222' test.txt twpower@192.168.1.2:~/test.txt


~/.ssh/config 파일을 사용하는 경우

  • 형태
Host test_host
    ...
    Port 2222
    ...
rsync [target file or directory] [host name]:[destination]
  • 예시
rsync test.txt test_host:~/test.txt


참고자료

Use different port not 22 in rsync


Environment and Prerequisite

  • Linux
  • Bash shell(/bin/bash)
  • rsync


rsync

Method

  • Use -e option
  • All of the examples below are written to use port 2222
-e 'ssh -p 2222'


In case when use IP address or domain

  • Format
rsync -e 'ssh -p 2222' [target file or directory] [user]@[IP address or domain]:[destination]
  • Example
rsync -e 'ssh -p 2222' test.txt twpower@192.168.1.2:~/test.txt


In case when use ~/.ssh/config file

  • Format
Host test_host
    ...
    Port 2222
    ...
rsync [target file or directory] [host name]:[destination]
  • Example
rsync test.txt test_host:~/test.txt


Reference

Mockito를 사용해 Spring Service를 테스트 해보자


환경

  • Java
  • Spring
  • JUnit5
  • SpringExtension.class


Mockito를 통한 단위 테스트

Mockito

  • Mockito: Java에서 모킹(Mocking)을 지원해주는 테스트 프레임워크
  • @Mock: 테스트에 필요한 객체를 모킹(Mocking)해주는 어노테이션
  • @InjectMocks: 테스트에 필요한 객체를 모킹(Mocking)해주는것과 함께 @Mock으로 모킹된 객체들 중에서 필요한 객체들을 주입해주는 어노테이션
  • when(method_name).thenReturn(return_value);: 모킹(Mocking)된 객체의 특정 메소드(method_name) 호출시 return_value를 반환하도록 설정 가능한 메소드


예제

build.gradle

plugins {
	id 'org.springframework.boot' version '2.5.13'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'org.twpower'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/milestone' }
	maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-data-jpa')
	implementation('org.springframework.boot:spring-boot-starter-web')

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	runtimeOnly 'com.h2database:h2'

	testImplementation('org.springframework.boot:spring-boot-starter-test')
	testImplementation('org.junit.jupiter:junit-jupiter')
	testImplementation('org.junit.jupiter:junit-jupiter-api')
	testImplementation('org.mockito:mockito-core')
	testImplementation('org.mockito:mockito-junit-jupiter')
	testImplementation('org.hamcrest:hamcrest')
	testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}

test {
	useJUnitPlatform()
}

Entity

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "email")})
public class UserEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;
}

JPA Repository

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    UserEntity findByEmail(String email);
    Boolean existsByEmail(String email);
}

Service

@Slf4j
@Service
public class UserService {

    @Autowired
    UserRepository userRepository;

    public UserEntity create(final UserEntity userEntity){
        if (userEntity == null || userEntity.getEmail() == null) {
            throw new RuntimeException("Invalid Arguments");
        }

        final String email = userEntity.getEmail();
        if (userRepository.existsByEmail(email)) {
            log.warn("Email already exists {}", email);
            throw new RuntimeException("Email already exists");
        }

        return userRepository.save(userEntity);
    }

}

Test

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void createTest() {
        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.create(null);
        });

        UserEntity userEntityForNullEmail = UserEntity.builder().build();
        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.create(userEntityForNullEmail);
        });

        UserEntity userEntityForExistUser = UserEntity.builder()
                .email("test@test.com")
                .username("testUsername")
                .password("testPassword")
                .build();
        when(userRepository.existsByEmail(any())).thenReturn(true);
        Assertions.assertThrows(RuntimeException.class, () -> {
            userService.create(userEntityForExistUser);
        });

        UserEntity userEntity = UserEntity.builder()
                .email("test@test.com")
                .username("testUsername")
                .password("testPassword")
                .build();
        reset(userRepository);
        when(userRepository.save(any())).thenReturn(userEntity);
        UserEntity returnedUserEntity = userService.create(userEntity);
        Assertions.assertEquals("test@test.com", returnedUserEntity.getEmail());
        Assertions.assertEquals("testUsername", returnedUserEntity.getUsername());
        Assertions.assertEquals("testPassword", returnedUserEntity.getPassword());
    }
}


참고자료