Open vcho1958 opened 1 year ago
문제 발생하는 코드를 예로 들어 설명해 주면 나을 것 같습니다. 위에 설명이 있으니, 예제 코드만 넣어주면 될 것으로 보입니다. 필요하다면, 코멘트 추가해 주시고요.
참고로, collection에서 element 값들은 모두 동일 유형(타입)의 객체이어야 합니다.
말씀하신대로 Map 자료형의 요소들마다 다른 유형을 넣어서 발생한 버그인 것 같습니다. 세션의 경우 각 필드별로 변경요소가 많을 수 있어서 서로 다른 자료형을 하나의 Map콜렉션에 집어넣을 필요성이 있었습니다.
다음은 문제가 된 데이터가 생성되는 과정입니다.
this.delta.put(ArcusSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(ArcusSessionMapper.MAX_INACTIVE_INTERVAL_KEY, cached.getMaxInactiveInterval().getSeconds());
this.delta.put(ArcusSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
이런 자료가 arcus에 추가가 되고
public class User implements UserDetails, Serializable {
private static final long serialVersionUID = -1;
@Id
@GeneratedValue
private Long id;
private String password;
private String username;
@OneToMany(mappedBy = "user")
private List<UserRole> roles = new ArrayList<>();
@JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss")
private LocalDateTime lastAccessedAt;
private boolean locked = false;
@Transient
private Duration maxInactiveInterval = Duration.ofSeconds(3600);
@Transient
private Instant lastAccessedTime = Instant.now();
static public User of(String username, String encodedPassword) {
return User.builder()
.username(username)
.password(encodedPassword)
.createdAt(LocalDateTime.now())
.lastAccessedAt(LocalDateTime.now())
.locked(false)
.build();
}
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<Role> roles = new ArrayList<>();
this.roles.forEach(role -> roles.add(role.getRole()));
return roles;
}
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
Duration maxAccessInterval = Duration.ofSeconds(7776000); //90일
return true;
}
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return !locked;
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JsonIgnore
public boolean isEnabled() {
return isAccountNonExpired() && isAccountNonLocked() && isCredentialsNonExpired();
}
}
이런 자료구조가
@PostMapping("/auth/login")
public boolean login(@RequestBody LoginDto loginDto, HttpSession session){
log.info(loginDto.toString());
UserDetails user = authService.login(loginDto);
session.setAttribute("principal", user);
return true;
}
setAttribute가 끝난 후 해당 콜렉션에 삽입됩니다.
Arcus에 저장될 때는
this.delta.forEach((dkey, value)->{
Boolean response = null;
try {
response = ArcusSessionRepository.this.arcusClientPool.asyncMopInsert(key, dkey, value, collectionAttributes).get();
if(!response)
ArcusSessionRepository.this.arcusClientPool.asyncMopUpdate(key,dkey,value).get();
ArcusSessionRepository.this.arcusClientPool.asyncSetAttr(key, collectionAttributes).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
});
this.delta.clear();
이런 코드에 의해 저장이 됐습니다.
이후 다시 불러올 땐
public ArcusSession findById(String sessionId) {
String key = getSessionKey(sessionId);
Map<String, Object> entries = null;
try {
entries = toDeserializedMap(this.arcusClientPool.asyncMopGet(key,false,false).get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (entries == null || entries.isEmpty()) {
return null;
}
MapSession session = new ArcusSessionMapper(sessionId).apply(entries);
if (session.isExpired()) {
deleteById(sessionId);
return null;
}
return new ArcusSession(session, false);
}
이런 코드로 불러와서
public MapSession apply(Map<String, Object> map) {
Assert.notEmpty(map, "map must not be empty");
MapSession session = new MapSession(this.sessionId);
Long creationTime = (Long) map.get(CREATION_TIME_KEY);
if (creationTime == null) {
handleMissingKey(CREATION_TIME_KEY);
}
session.setCreationTime(Instant.ofEpochMilli(creationTime));
Long lastAccessedTime = (Long) map.get(LAST_ACCESSED_TIME_KEY);
if (lastAccessedTime == null) {
handleMissingKey(LAST_ACCESSED_TIME_KEY);
}
session.setLastAccessedTime(Instant.ofEpochMilli(lastAccessedTime));
Long maxInactiveInterval = (int) map.get(MAX_INACTIVE_INTERVAL_KEY);
if (maxInactiveInterval == null) {
handleMissingKey(MAX_INACTIVE_INTERVAL_KEY);
}
session.setMaxInactiveInterval(Duration.ofSeconds(maxInactiveInterval));
map.forEach((name, value) -> {
if (name.startsWith(ATTRIBUTE_PREFIX)) {
session.setAttribute(name.substring(ATTRIBUTE_PREFIX.length()), value);
}
});
return session;
}
apply함수에 의해 세션 인스턴스로 매핑되는 과정 중 (int) map.get(MAX_INACTIVE_INTERVAL_KEY);
에 실패했다는 메세지가 발생합니다.
명칭은 잘 기억이 안나지만 잭슨의 deserializer처럼 유사한 역할을 하는 클래스가 있었던 것 같은데 문서에서 찾지 못했습니다. Transcoder였던것 같은데 확장 방법에 대한 매뉴얼이 있을까요?
필요시 JPA의 어노테이션처럼 대상이되는 클래스에 추가하면 리플렉션을 이용해 컴파일시점에 해당 deserializer를 자동 구현하게하는 어노테이션을 추가하겠습니다.
@vcho1958 arcus-java-client API 기준으로 상황을 설명해 주어야 이해하기 편할 것 같습니다.
arcusClientPool.asyncMopInsert(key, dkey, value, collectionAttributes);
를 통해 값들을 넣었을 때 모든 필드에 대한 타입이 가장 먼저 들어가는 값(A)의 타입으로 강제 지정됩니다.
arcusClientPool.asyncMopGet(key,false,false)
이후 조회하여 원래 클래스로 형변환을 시도하면 A를 B로 형변환에 실패했다는 메세지가 출력됩니다.
arcusClientPool.asyncMopCreate(key, ElementValueType.OTHERS, collectionAttributes)
생성 후 시도하면 조회시 모든 타입에서 caught ioexception decoding 2(자료형의 크기) bytes of data
에러로그가 출력되고 null이 반환됩니다.
쉽게 말하자면 Map<String, Object>
타입을 arcus에 저장할 수 없다는 의미입니다.
Arcus의 Map은 정해진 하나의 타입만 넣을 수 있고, 이 이슈에서는 여러 타입(원시 타입 포함)의 객체들을 넣고 싶은 경우이기 때문입니다.
현재 저장하는 객체의 타입을 cache item의 flags 정보로 기록해 두게 되는 데, 1개의 flags 정보만 cache item header에 둘 수 있습니다. 즉, 1개 객체 타입만 지정 가능합니다.
list, set, b+tree 경우에는 동일 타입의 Object 사용하는 것이 일반적인 데, map 경우는 서로 다른 타입의 Object 사용하는 경우가 많을 수 있겠네요.
1개 타입의 object 만 사용하도록 java client map API를 변경할 수 있나요? String 타입으로 고정하는 것이 하나의 방안인데, 응용에서 String이 아닌 동일 객체 타입의 객체만을 사용한다면 기존대로 Object 타입으로 하면 되구요. 아니면, 가이드 문서에 주의사항 코멘트를 남기는 것이 좋은가요?
@vcho1958
이슈 주제와 관련 없는 이야기지만, 앞으로는 https://github.com/naver/arcus-java-client 레포지토리에 이슈를 등록해주세요.
넵 주의사항 코멘트를 남기는 편이 좋을 것 같습니다. 이외에도 collection attribute를 insert할 때 지정하여 자동생성이 된 map이 아닌, create를 통해 타입을 OTHERS로 지정하면 위에서 말씀드린 것처럼 decode시에 오류가 발생합니다.
@jhpark816
아니면, 가이드 문서에 주의사항 코멘트를 남기는 것이 좋은가요?
1.13.4 릴리즈 전에 문서에 포함할까요?
@uhm0311 기존 interface에서 달라지는 부분이 아니므로, 1.13.4 릴리즈에선 그대로 두시죠. 하지만, 나중에 논의해야 할 주제라고 생각합니다.
CollectionAttributes에 만료기간만 정한 후 insert에 해당 attribute를 넣어 생성과 함께 필드를 할당하면,
이후 다시 가져왔을 때 가장 먼저 집어넣은 키, 값 중 값의 클래스로 고정되어 인식됩니다. 실제 케이스에선 Long이 가장 먼저 삽입되었습니다.
정상적인 방법으로 OTHERS 타입으로 생성 및 값 입력
실제 자료형은 각각 long, integer, long 순입니다. 으로 입력되었으며 caught ioexception decoding 2(위 예시의 숫자) bytes of data가 항목마다 발생했습니다. 조치방법: STRING형으로 생성 후, JSON으로 직렬화 후 가져올 때마다 다시 역직렬화하는 방식으로 대체했습니다.